Skip to main content

script/
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::ffi::CStr;
11use std::fmt::Debug;
12use std::ptr::NonNull;
13use std::rc::Rc;
14use std::{mem, ptr};
15
16use encoding_rs::UTF_8;
17use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
18use hyper_serde::Serde;
19use indexmap::IndexMap;
20use indexmap::map::Entry;
21use js::context::JSContext;
22use js::conversions::jsstr_to_string;
23use js::gc::{HandleObject, MutableHandleValue};
24use js::jsapi::{
25    CallArgs, ExceptionStackBehavior, GetFunctionNativeReserved, GetModuleResolveHook,
26    Handle as RawHandle, HandleValue as RawHandleValue, Heap, JS_GetFunctionObject,
27    JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE, JSRuntime, ModuleErrorBehaviour,
28    ModuleType, SetFunctionNativeReserved, SetModuleDynamicImportHook, SetModuleMetadataHook,
29    SetModulePrivate, SetModuleResolveHook, SetScriptPrivateReferenceHooks, Value,
30};
31use js::jsval::{JSVal, PrivateValue, UndefinedValue};
32use js::realm::{AutoRealm, CurrentRealm};
33use js::rust::wrappers2::{
34    CompileJsonModule1, CompileModule1, DefineFunctionWithReserved, GetModuleRequestSpecifier,
35    GetModuleRequestType, JS_ClearPendingException, JS_DefineProperty4, JS_GetPendingException,
36    JS_NewStringCopyN, JS_SetPendingException, ModuleEvaluate, ModuleLink,
37    ThrowOnModuleEvaluationFailure,
38};
39use js::rust::{Handle, HandleValue, ToString, transform_str_to_source_text};
40use mime::Mime;
41use net_traits::http_status::HttpStatus;
42use net_traits::mime_classifier::MimeClassifier;
43use net_traits::policy_container::PolicyContainer;
44use net_traits::request::{
45    CredentialsMode, Destination, ParserMetadata, Referrer, RequestBuilder, RequestClient,
46    RequestId, RequestMode,
47};
48use net_traits::{FetchMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming};
49use script_bindings::cell::DomRefCell;
50use script_bindings::error::Fallible;
51use script_bindings::reflector::DomObject;
52use script_bindings::settings_stack::run_a_callback;
53use script_bindings::trace::CustomTraceable;
54use serde_json::{Map as JsonMap, Value as JsonValue};
55use servo_config::pref;
56use servo_url::ServoUrl;
57
58use crate::DomTypeHolder;
59use crate::dom::bindings::conversions::SafeToJSValConvertible;
60use crate::dom::bindings::error::{
61    Error, ErrorToJsval, report_pending_exception, throw_dom_exception,
62};
63use crate::dom::bindings::inheritance::Castable;
64use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
65use crate::dom::bindings::root::DomRoot;
66use crate::dom::bindings::str::DOMString;
67use crate::dom::bindings::trace::RootedTraceableBox;
68use crate::dom::csp::{GlobalCspReporting, Violation};
69use crate::dom::globalscope::GlobalScope;
70use crate::dom::globalscope::script_execution::{ErrorReporting, fill_compile_options};
71use crate::dom::html::htmlscriptelement::{SCRIPT_JS_MIMES, substitute_with_local_script};
72use crate::dom::performance::performanceresourcetiming::InitiatorType;
73use crate::dom::promise::Promise;
74use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
75use crate::dom::types::{
76    Console, DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, WorkerGlobalScope,
77};
78use crate::dom::window::Window;
79use crate::module_loading::{
80    LoadState, Payload, host_load_imported_module, load_requested_modules,
81};
82use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
83use crate::realms::enter_auto_realm;
84use crate::script_runtime::IntroductionType;
85use crate::task::NonSendTaskBox;
86use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
87
88pub(crate) fn gen_type_error(
89    cx: &mut JSContext,
90    global: &GlobalScope,
91    error: Error,
92) -> RethrowError {
93    rooted!(&in(cx) let mut thrown = UndefinedValue());
94    error.to_jsval(cx, global, thrown.handle_mut());
95
96    RethrowError(RootedTraceableBox::from_box(Heap::boxed(thrown.get())))
97}
98
99#[derive(JSTraceable)]
100pub(crate) struct ModuleObject(RootedTraceableBox<Heap<*mut JSObject>>);
101
102impl ModuleObject {
103    pub(crate) fn new(obj: HandleObject) -> ModuleObject {
104        ModuleObject(RootedTraceableBox::from_box(Heap::boxed(obj.get())))
105    }
106
107    pub(crate) fn handle(&'_ self) -> HandleObject<'_> {
108        self.0.handle()
109    }
110}
111
112#[derive(JSTraceable)]
113pub(crate) struct RethrowError(RootedTraceableBox<Heap<JSVal>>);
114
115impl RethrowError {
116    pub(crate) fn new(val: Box<Heap<JSVal>>) -> Self {
117        Self(RootedTraceableBox::from_box(val))
118    }
119
120    #[expect(unsafe_code)]
121    pub(crate) fn from_pending_exception(cx: &mut JSContext) -> Self {
122        rooted!(&in(cx) let mut exception = UndefinedValue());
123        assert!(unsafe { JS_GetPendingException(cx, exception.handle_mut()) });
124        unsafe { JS_ClearPendingException(cx) };
125
126        Self::new(Heap::boxed(exception.get()))
127    }
128
129    pub(crate) fn handle(&self) -> Handle<'_, JSVal> {
130        self.0.handle()
131    }
132}
133
134impl Debug for RethrowError {
135    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
136        "RethrowError(...)".fmt(fmt)
137    }
138}
139
140impl Clone for RethrowError {
141    fn clone(&self) -> Self {
142        Self(RootedTraceableBox::from_box(Heap::boxed(self.0.get())))
143    }
144}
145
146pub(crate) struct ModuleScript {
147    pub(crate) base_url: ServoUrl,
148    pub(crate) options: ScriptFetchOptions,
149    pub(crate) owner: Option<Trusted<GlobalScope>>,
150}
151
152impl ModuleScript {
153    pub(crate) fn new(
154        base_url: ServoUrl,
155        options: ScriptFetchOptions,
156        owner: Option<Trusted<GlobalScope>>,
157    ) -> Self {
158        ModuleScript {
159            base_url,
160            options,
161            owner,
162        }
163    }
164}
165
166pub(crate) type ModuleRequest = (ServoUrl, ModuleType);
167
168#[derive(Clone, JSTraceable)]
169pub(crate) enum ModuleStatus {
170    Fetching(DomRefCell<Option<Rc<Promise>>>),
171    Loaded(Option<Rc<ModuleTree>>),
172}
173
174#[derive(JSTraceable, MallocSizeOf)]
175pub(crate) struct ModuleTree {
176    #[no_trace]
177    url: ServoUrl,
178    #[ignore_malloc_size_of = "mozjs"]
179    record: OnceCell<ModuleObject>,
180    #[ignore_malloc_size_of = "mozjs"]
181    parse_error: OnceCell<RethrowError>,
182    #[ignore_malloc_size_of = "mozjs"]
183    rethrow_error: DomRefCell<Option<RethrowError>>,
184    #[no_trace]
185    loaded_modules: DomRefCell<IndexMap<String, ServoUrl>>,
186}
187
188impl ModuleTree {
189    pub(crate) fn get_url(&self) -> ServoUrl {
190        self.url.clone()
191    }
192
193    pub(crate) fn get_record(&self) -> Option<&ModuleObject> {
194        self.record.get()
195    }
196
197    pub(crate) fn get_parse_error(&self) -> Option<&RethrowError> {
198        self.parse_error.get()
199    }
200
201    pub(crate) fn get_rethrow_error(&self) -> &DomRefCell<Option<RethrowError>> {
202        &self.rethrow_error
203    }
204
205    pub(crate) fn set_rethrow_error(&self, rethrow_error: RethrowError) {
206        *self.rethrow_error.borrow_mut() = Some(rethrow_error);
207    }
208
209    pub(crate) fn find_descendant_inside_module_map(
210        &self,
211        global: &GlobalScope,
212        specifier: &String,
213        module_type: ModuleType,
214    ) -> Option<Rc<ModuleTree>> {
215        self.loaded_modules
216            .borrow()
217            .get(specifier)
218            .and_then(|url| global.get_module_map_entry(&(url.clone(), module_type)))
219            .and_then(|status| match status {
220                ModuleStatus::Fetching(_) => None,
221                ModuleStatus::Loaded(module_tree) => module_tree,
222            })
223    }
224
225    pub(crate) fn insert_module_dependency(
226        &self,
227        module: &Rc<ModuleTree>,
228        module_request_specifier: String,
229    ) {
230        // Store the url which is used to retrieve the module from module map when needed.
231        let url = module.url.clone();
232        match self
233            .loaded_modules
234            .borrow_mut()
235            .entry(module_request_specifier)
236        {
237            // a. If referrer.[[LoadedModules]] contains a LoadedModuleRequest Record record such that
238            // ModuleRequestsEqual(record, moduleRequest) is true, then
239            Entry::Occupied(entry) => {
240                // i. Assert: record.[[Module]] and result.[[Value]] are the same Module Record.
241                assert_eq!(*entry.get(), url);
242            },
243            // b. Else,
244            Entry::Vacant(entry) => {
245                // i. Append the LoadedModuleRequest Record { [[Specifier]]: moduleRequest.[[Specifier]],
246                // [[Attributes]]: moduleRequest.[[Attributes]], [[Module]]: result.[[Value]] } to referrer.[[LoadedModules]].
247                entry.insert(url);
248            },
249        }
250    }
251}
252
253pub(crate) struct ModuleSource<'a> {
254    pub source: Cow<'a, str>,
255    pub unminified_dir: Option<String>,
256    pub external: bool,
257    pub url: ServoUrl,
258}
259
260impl<'a> crate::unminify::ScriptSource for ModuleSource<'a> {
261    fn unminified_dir(&self) -> Option<String> {
262        self.unminified_dir.clone()
263    }
264
265    fn extract_bytes(&self) -> &[u8] {
266        self.source.as_bytes()
267    }
268
269    fn rewrite_source(&mut self, source: String) {
270        self.source = source.into();
271    }
272
273    fn url(&self) -> ServoUrl {
274        self.url.clone()
275    }
276
277    fn is_external(&self) -> bool {
278        self.external
279    }
280}
281
282impl ModuleTree {
283    #[expect(unsafe_code)]
284    #[expect(clippy::too_many_arguments)]
285    /// <https://html.spec.whatwg.org/multipage/#creating-a-javascript-module-script>
286    fn create_a_javascript_module_script(
287        cx: &mut JSContext,
288        source: Cow<'_, str>,
289        global: &GlobalScope,
290        url: &ServoUrl,
291        options: ScriptFetchOptions,
292        external: bool,
293        line_number: u32,
294        introduction_type: Option<&'static CStr>,
295    ) -> Self {
296        let mut realm = AutoRealm::new(
297            cx,
298            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
299        );
300        let cx = &mut *realm;
301
302        let owner = Trusted::new(global);
303
304        // Step 2. Let script be a new module script that this algorithm will subsequently initialize.
305        // Step 6. Set script's parse error and error to rethrow to null.
306        let module = ModuleTree {
307            url: url.clone(),
308            record: OnceCell::new(),
309            parse_error: OnceCell::new(),
310            rethrow_error: DomRefCell::new(None),
311            loaded_modules: DomRefCell::new(IndexMap::new()),
312        };
313
314        let compile_options = fill_compile_options(
315            cx,
316            url.as_str(),
317            introduction_type,
318            ErrorReporting::Unmuted,
319            true, // noScriptRval
320            line_number,
321        );
322
323        let mut source = if global.unminified_js_dir().is_some() {
324            let mut module_source = ModuleSource {
325                source,
326                unminified_dir: global.unminified_js_dir(),
327                external,
328                url: url.clone(),
329            };
330            crate::unminify::unminify_js(&mut module_source);
331            transform_str_to_source_text(&module_source.source)
332        } else {
333            transform_str_to_source_text(&source)
334        };
335
336        unsafe {
337            // Step 7. Let result be ParseModule(source, settings's realm, script).
338            rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
339            module_script.set(CompileModule1(cx, compile_options.ptr, &mut source));
340
341            // Step 8. If result is a list of errors, then:
342            if module_script.is_null() {
343                warn!("fail to compile module script of {}", url);
344
345                // Step 8.1. Set script's parse error to result[0].
346                let _ = module
347                    .parse_error
348                    .set(RethrowError::from_pending_exception(cx));
349
350                // Step 8.2. Return script.
351                return module;
352            }
353
354            // Step 3. Set script's settings object to settings.
355            // Step 4. Set script's base URL to baseURL.
356            // Step 5. Set script's fetch options to options.
357            let module_script_data = Rc::new(ModuleScript::new(url.clone(), options, Some(owner)));
358
359            SetModulePrivate(
360                module_script.get(),
361                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
362            );
363
364            // Step 9. Set script's record to result.
365            let _ = module.record.set(ModuleObject::new(module_script.handle()));
366        }
367
368        // Step 10. Return script.
369        module
370    }
371
372    #[expect(unsafe_code)]
373    /// <https://html.spec.whatwg.org/multipage/#creating-a-json-module-script>
374    fn create_a_json_module_script(
375        cx: &mut JSContext,
376        source: &str,
377        global: &GlobalScope,
378        url: &ServoUrl,
379        introduction_type: Option<&'static CStr>,
380    ) -> Self {
381        let mut realm = AutoRealm::new(
382            cx,
383            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
384        );
385        let cx = &mut *realm;
386
387        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
388        // Step 4. Set script's parse error and error to rethrow to null.
389        let module = ModuleTree {
390            url: url.clone(),
391            record: OnceCell::new(),
392            parse_error: OnceCell::new(),
393            rethrow_error: DomRefCell::new(None),
394            loaded_modules: DomRefCell::new(IndexMap::new()),
395        };
396
397        // Step 2. Set script's settings object to settings.
398        // Step 3. Set script's base URL and fetch options to null.
399        // Note: We don't need to call `SetModulePrivate` for json scripts
400
401        let compile_options = fill_compile_options(
402            cx,
403            url.as_str(),
404            introduction_type,
405            ErrorReporting::Unmuted,
406            true, // noScriptRval
407            1,    // lineno
408        );
409
410        rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
411
412        unsafe {
413            // Step 5. Let result be ParseJSONModule(source).
414            module_script.set(CompileJsonModule1(
415                cx,
416                compile_options.ptr,
417                &mut transform_str_to_source_text(source),
418            ));
419        }
420
421        // If this throws an exception, catch it, and set script's parse error to that exception, and return script.
422        if module_script.is_null() {
423            warn!("fail to compile module script of {}", url);
424
425            let _ = module
426                .parse_error
427                .set(RethrowError::from_pending_exception(cx));
428            return module;
429        }
430
431        // Step 6. Set script's record to result.
432        let _ = module.record.set(ModuleObject::new(module_script.handle()));
433
434        // Step 7. Return script.
435        module
436    }
437
438    /// Execute the provided module, storing the evaluation return value in the provided
439    /// mutable handle.
440    #[expect(unsafe_code)]
441    pub(crate) fn execute_module(
442        &self,
443        cx: &mut JSContext,
444        global: &GlobalScope,
445        module_record: HandleObject,
446        mut eval_result: MutableHandleValue,
447    ) -> Result<(), RethrowError> {
448        let mut realm = AutoRealm::new(
449            cx,
450            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
451        );
452        let cx = &mut *realm;
453
454        unsafe {
455            let ok = ModuleEvaluate(cx, module_record, eval_result.reborrow());
456            assert!(ok, "module evaluation failed");
457
458            rooted!(&in(cx) let mut evaluation_promise = ptr::null_mut::<JSObject>());
459            if eval_result.is_object() {
460                evaluation_promise.set(eval_result.to_object());
461            }
462
463            let throw_result = ThrowOnModuleEvaluationFailure(
464                cx,
465                evaluation_promise.handle(),
466                ModuleErrorBehaviour::ThrowModuleErrorsSync,
467            );
468            if !throw_result {
469                warn!("fail to evaluate module");
470
471                Err(RethrowError::from_pending_exception(cx))
472            } else {
473                debug!("module evaluated successfully");
474                Ok(())
475            }
476        }
477    }
478
479    #[expect(unsafe_code)]
480    pub(crate) fn report_error(&self, cx: &mut JSContext, global: &GlobalScope) {
481        let module_error = self.rethrow_error.borrow();
482
483        if let Some(exception) = &*module_error {
484            let mut realm = enter_auto_realm(cx, global);
485            let cx = &mut realm.current_realm();
486
487            unsafe {
488                JS_SetPendingException(cx, exception.handle(), ExceptionStackBehavior::Capture);
489            }
490            report_pending_exception(cx);
491        }
492    }
493
494    /// <https://html.spec.whatwg.org/multipage/#resolve-a-module-specifier>
495    pub(crate) fn resolve_module_specifier(
496        global: &GlobalScope,
497        script: Option<&ModuleScript>,
498        specifier: DOMString,
499    ) -> Fallible<ServoUrl> {
500        // Step 1~3 to get settingsObject and baseURL
501        let script_global = script.and_then(|s| s.owner.as_ref().map(|o| o.root()));
502        // Step 1. Let settingsObject and baseURL be null.
503        let (global, base_url): (&GlobalScope, &ServoUrl) = match script {
504            // Step 2. If referringScript is not null, then:
505            // Set settingsObject to referringScript's settings object.
506            // Set baseURL to referringScript's base URL.
507            Some(s) => (script_global.as_ref().map_or(global, |g| g), &s.base_url),
508            // Step 3. Otherwise:
509            // Set settingsObject to the current settings object.
510            // Set baseURL to settingsObject's API base URL.
511            // FIXME(#37553): Is this the correct current settings object?
512            None => (global, &global.api_base_url()),
513        };
514
515        // Step 4. Let importMap be an empty import map.
516        // Step 5. If settingsObject's global object implements Window, then set importMap to settingsObject's
517        // global object's import map.
518        let import_map = if global.is::<Window>() {
519            Some(global.import_map())
520        } else {
521            None
522        };
523        let specifier = &specifier.str();
524
525        // Step 6. Let serializedBaseURL be baseURL, serialized.
526        let serialized_base_url = base_url.as_str();
527        // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
528        let as_url = Self::resolve_url_like_module_specifier(specifier, base_url);
529        // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null;
530        // otherwise, specifier.
531        let normalized_specifier = match &as_url {
532            Some(url) => url.as_str(),
533            None => specifier,
534        };
535
536        // Step 9. Let result be a URL-or-null, initially null.
537        let mut result = None;
538        if let Some(map) = import_map {
539            // Step 10. For each scopePrefix → scopeImports of importMap's scopes:
540            for (prefix, imports) in &map.scopes {
541                // Step 10.1 If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/)
542                // and scopePrefix is a code unit prefix of serializedBaseURL, then:
543                let prefix = prefix.as_str();
544                if prefix == serialized_base_url ||
545                    (serialized_base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
546                {
547                    // Step 10.1.1 Let scopeImportsMatch be the result of resolving an imports match
548                    // given normalizedSpecifier, asURL, and scopeImports.
549                    let scope_imports_match =
550                        resolve_imports_match(normalized_specifier, as_url.as_ref(), imports)?;
551
552                    // Step 10.1.2 If scopeImportsMatch is not null, then set result to scopeImportsMatch, and break.
553                    if scope_imports_match.is_some() {
554                        result = scope_imports_match;
555                        break;
556                    }
557                }
558            }
559
560            // Step 11. If result is null, set result to the result of resolving an imports match given
561            // normalizedSpecifier, asURL, and importMap's imports.
562            if result.is_none() {
563                result =
564                    resolve_imports_match(normalized_specifier, as_url.as_ref(), &map.imports)?;
565            }
566        }
567
568        // Step 12. If result is null, set it to asURL.
569        if result.is_none() {
570            result = as_url.clone();
571        }
572
573        // Step 13. If result is not null, then:
574        match result {
575            Some(result) => {
576                // Step 13.1 Add module to resolved module set given settingsObject, serializedBaseURL,
577                // normalizedSpecifier, and asURL.
578                global.add_module_to_resolved_module_set(
579                    serialized_base_url,
580                    normalized_specifier,
581                    as_url.clone(),
582                );
583                // Step 13.2 Return result.
584                Ok(result)
585            },
586            // Step 14. Throw a TypeError indicating that specifier was a bare specifier,
587            // but was not remapped to anything by importMap.
588            None => Err(Error::Type(
589                c"Specifier was a bare specifier, but was not remapped to anything by importMap."
590                    .to_owned(),
591            )),
592        }
593    }
594
595    /// <https://html.spec.whatwg.org/multipage/#resolving-a-url-like-module-specifier>
596    fn resolve_url_like_module_specifier(specifier: &str, base_url: &ServoUrl) -> Option<ServoUrl> {
597        // Step 1. If specifier starts with "/", "./", or "../", then:
598        if specifier.starts_with('/') || specifier.starts_with("./") || specifier.starts_with("../")
599        {
600            // Step 1.1. Let url be the result of URL parsing specifier with baseURL.
601            return ServoUrl::parse_with_base(Some(base_url), specifier).ok();
602        }
603        // Step 2. Let url be the result of URL parsing specifier (with no base URL).
604        ServoUrl::parse(specifier).ok()
605    }
606}
607
608#[derive(JSTraceable, MallocSizeOf)]
609pub(crate) struct ModuleHandler {
610    #[ignore_malloc_size_of = "Measuring trait objects is hard"]
611    task: DomRefCell<Option<Box<dyn NonSendTaskBox>>>,
612}
613
614impl ModuleHandler {
615    pub(crate) fn new_boxed(task: Box<dyn NonSendTaskBox>) -> Box<dyn Callback> {
616        Box::new(Self {
617            task: DomRefCell::new(Some(task)),
618        })
619    }
620}
621
622impl Callback for ModuleHandler {
623    fn callback(&self, cx: &mut CurrentRealm, _v: HandleValue) {
624        let task = self.task.borrow_mut().take().unwrap();
625        task.run_box(cx);
626    }
627}
628
629#[derive(JSTraceable, MallocSizeOf)]
630struct QueueTaskHandler {
631    #[conditional_malloc_size_of]
632    promise: Rc<Promise>,
633}
634
635impl Callback for QueueTaskHandler {
636    fn callback(&self, cx: &mut CurrentRealm, _: HandleValue) {
637        let global = GlobalScope::from_current_realm(cx);
638        let promise = TrustedPromise::new(self.promise.clone());
639
640        global.task_manager().networking_task_source().queue(
641            task!(continue_module_loading: move |cx| {
642                promise.root().resolve_native(cx, &());
643            }),
644        );
645    }
646}
647
648/// The context required for asynchronously loading an external module script source.
649struct ModuleContext {
650    /// The owner of the module that initiated the request.
651    owner: Trusted<GlobalScope>,
652    /// The response body received to date.
653    data: Vec<u8>,
654    /// The response metadata received to date.
655    metadata: Option<Metadata>,
656    /// Url and type of the requested module.
657    module_request: ModuleRequest,
658    /// Options for the current script fetch
659    options: ScriptFetchOptions,
660    /// Indicates whether the request failed, and why
661    status: Result<(), NetworkError>,
662    /// `introductionType` value to set in the `CompileOptionsWrapper`.
663    introduction_type: Option<&'static CStr>,
664    /// <https://html.spec.whatwg.org/multipage/#policy-container>
665    policy_container: Option<PolicyContainer>,
666}
667
668impl FetchResponseListener for ModuleContext {
669    // TODO(cybai): Perhaps add custom steps to perform fetch here?
670    fn process_request_body(&mut self, _: RequestId) {}
671
672    fn process_response(
673        &mut self,
674        _: &mut js::context::JSContext,
675        _: RequestId,
676        metadata: Result<FetchMetadata, NetworkError>,
677    ) {
678        self.metadata = metadata.ok().map(|meta| match meta {
679            FetchMetadata::Unfiltered(m) => m,
680            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
681        });
682
683        let status = self
684            .metadata
685            .as_ref()
686            .map(|m| m.status.clone())
687            .unwrap_or_else(HttpStatus::new_error);
688
689        self.status = {
690            if status.is_error() {
691                Err(NetworkError::ResourceLoadError(
692                    "No http status code received".to_owned(),
693                ))
694            } else if status.is_success() {
695                Ok(())
696            } else {
697                Err(NetworkError::ResourceLoadError(format!(
698                    "HTTP error code {}",
699                    status.code()
700                )))
701            }
702        };
703    }
704
705    fn process_response_chunk(
706        &mut self,
707        _: &mut js::context::JSContext,
708        _: RequestId,
709        mut chunk: Vec<u8>,
710    ) {
711        if self.status.is_ok() {
712            self.data.append(&mut chunk);
713        }
714    }
715
716    /// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
717    /// Step 13
718    fn process_response_eof(
719        mut self,
720        cx: &mut js::context::JSContext,
721        _: RequestId,
722        response: Result<(), NetworkError>,
723        timing: ResourceFetchTiming,
724    ) {
725        let global = self.owner.root();
726        let (_url, module_type) = &self.module_request;
727
728        network_listener::submit_timing(cx, &self, &response, &timing);
729
730        let Some(ModuleStatus::Fetching(pending)) =
731            global.get_module_map_entry(&self.module_request)
732        else {
733            return error!("Processing response for a non pending module request");
734        };
735        let promise = pending
736            .borrow_mut()
737            .take()
738            .expect("Need promise to process response");
739
740        // Step 1. If any of the following are true: bodyBytes is null or failure; or response's status is not an ok status,
741        // then set moduleMap[(url, moduleType)] to null, run onComplete given null, and abort these steps.
742        if let (Err(error), _) | (_, Err(error)) = (response.as_ref(), self.status.as_ref()) {
743            error!("Fetching module script failed {:?}", error);
744            global.set_module_map(self.module_request, ModuleStatus::Loaded(None));
745            return promise.resolve_native(cx, &());
746        }
747
748        let metadata = self.metadata.take().unwrap();
749
750        // The processResponseConsumeBody steps defined inside
751        // [run a worker](https://html.spec.whatwg.org/multipage/#run-a-worker)
752        if let Some(policy_container) = self.policy_container {
753            let workerscope = global.downcast::<WorkerGlobalScope>().expect(
754                "We only need a policy container when initializing a worker's globalscope.",
755            );
756            workerscope.process_response_for_workerscope(&metadata, &policy_container);
757        }
758
759        let final_url = metadata.final_url;
760
761        // Step 2. Let mimeType be the result of extracting a MIME type from response's header list.
762        let mime_type: Option<Mime> = metadata.content_type.map(Serde::into_inner).map(Into::into);
763
764        // Step 3. Let moduleScript be null.
765        let mut module_script = None;
766
767        // Step 4. Let referrerPolicy be the result of parsing the `Referrer-Policy` header given response. [REFERRERPOLICY]
768        let referrer_policy = metadata
769            .headers
770            .and_then(|headers| headers.typed_get::<ReferrerPolicyHeader>())
771            .into();
772
773        // Step 5. If referrerPolicy is not the empty string, set options's referrer policy to referrerPolicy.
774        if referrer_policy != ReferrerPolicy::EmptyString {
775            self.options.referrer_policy = referrer_policy;
776        }
777
778        // TODO Step 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set
779        // moduleScript to the result of creating a WebAssembly module script given bodyBytes, settingsObject, response's URL, and options.
780
781        // TODO handle CSS module scripts on the next mozjs ESR bump.
782
783        if let Some(mime) = mime_type {
784            // Step 7.1 Let sourceText be the result of UTF-8 decoding bodyBytes.
785            let (mut source_text, _) = UTF_8.decode_with_bom_removal(&self.data);
786
787            // Step 7.2 If mimeType is a JavaScript MIME type and moduleType is "javascript-or-wasm", then set moduleScript
788            // to the result of creating a JavaScript module script given sourceText, settingsObject, response's URL, and options.
789            if SCRIPT_JS_MIMES.contains(&mime.essence_str()) &&
790                matches!(module_type, ModuleType::JavaScript)
791            {
792                if let Some(window) = global.downcast::<Window>() &&
793                    let Some(script_souce) = window.local_script_source()
794                {
795                    substitute_with_local_script(script_souce, &mut source_text, final_url.clone());
796                }
797
798                let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
799                    cx,
800                    source_text,
801                    &global,
802                    &final_url,
803                    self.options,
804                    true,
805                    1,
806                    self.introduction_type,
807                ));
808                module_script = Some(module_tree);
809            } else if MimeClassifier::is_json(&mime) && matches!(module_type, ModuleType::JSON) {
810                // Step 7.4 If mimeType is a JSON MIME type and moduleType is "json",
811                // then set moduleScript to the result of creating a JSON module script given sourceText and settingsObject.
812                let module_tree = Rc::new(ModuleTree::create_a_json_module_script(
813                    cx,
814                    &source_text,
815                    &global,
816                    &final_url,
817                    self.introduction_type,
818                ));
819                module_script = Some(module_tree);
820            }
821        }
822        // Step 8. Set moduleMap[(url, moduleType)] to moduleScript, and run onComplete given moduleScript.
823        global.set_module_map(self.module_request, ModuleStatus::Loaded(module_script));
824        promise.resolve_native(cx, &());
825    }
826
827    fn process_csp_violations(
828        &mut self,
829        cx: &mut js::context::JSContext,
830        _request_id: RequestId,
831        violations: Vec<Violation>,
832    ) {
833        let global = self.owner.root();
834        if let Some(scope) = global.downcast::<DedicatedWorkerGlobalScope>() {
835            scope.report_csp_violations(violations);
836        } else if let Some(scope) = global.downcast::<SharedWorkerGlobalScope>() {
837            scope.report_csp_violations(violations);
838        } else {
839            global.report_csp_violations(cx, violations, None, None);
840        }
841    }
842
843    fn process_content_length(&mut self, _request_id: RequestId, size: usize) {
844        self.data.reserve(size - self.data.len());
845    }
846}
847
848impl ResourceTimingListener for ModuleContext {
849    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
850        let initiator_type = InitiatorType::LocalName("module".to_string());
851        let (url, _) = &self.module_request;
852        (initiator_type, url.clone())
853    }
854
855    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
856        self.owner.root()
857    }
858}
859
860#[expect(unsafe_code)]
861#[expect(non_snake_case)]
862/// A function to register module hooks (e.g. listening on resolving modules,
863/// getting module metadata, getting script private reference and resolving dynamic import)
864pub(crate) unsafe fn EnsureModuleHooksInitialized(rt: *mut JSRuntime) {
865    unsafe {
866        if GetModuleResolveHook(rt).is_some() {
867            return;
868        }
869
870        SetModuleResolveHook(rt, Some(HostResolveImportedModule));
871        SetModuleMetadataHook(rt, Some(HostPopulateImportMeta));
872        SetScriptPrivateReferenceHooks(
873            rt,
874            Some(host_add_ref_top_level_script),
875            Some(host_release_top_level_script),
876        );
877        SetModuleDynamicImportHook(rt, Some(host_import_module_dynamically));
878    }
879}
880
881#[expect(unsafe_code)]
882unsafe extern "C" fn host_add_ref_top_level_script(value: *const Value) {
883    let val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
884    mem::forget(val.clone());
885    mem::forget(val);
886}
887
888#[expect(unsafe_code)]
889unsafe extern "C" fn host_release_top_level_script(value: *const Value) {
890    let _val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
891}
892
893#[expect(unsafe_code)]
894/// <https://tc39.es/ecma262/#sec-hostimportmoduledynamically>
895/// <https://html.spec.whatwg.org/multipage/#hostimportmoduledynamically(referencingscriptormodule,-specifier,-promisecapability)>
896pub(crate) unsafe extern "C" fn host_import_module_dynamically(
897    cx: *mut RawJSContext,
898    reference_private: RawHandleValue,
899    specifier: RawHandle<*mut JSObject>,
900    promise: RawHandle<*mut JSObject>,
901) -> bool {
902    // SAFETY: it is safe to construct a JSContext from engine hook.
903    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
904    let cx = &mut cx;
905    let promise = Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise) });
906
907    let jsstr = unsafe { GetModuleRequestSpecifier(cx, Handle::from_raw(specifier)) };
908    let module_type = unsafe { GetModuleRequestType(cx, Handle::from_raw(specifier)) };
909    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
910
911    let mut realm = CurrentRealm::assert(cx);
912    let payload = Payload::PromiseRecord(promise);
913    host_load_imported_module(
914        &mut realm,
915        None,
916        reference_private,
917        specifier,
918        module_type,
919        None,
920        payload,
921    );
922
923    true
924}
925
926#[derive(Clone, Debug, JSTraceable, MallocSizeOf)]
927/// <https://html.spec.whatwg.org/multipage/#script-fetch-options>
928pub(crate) struct ScriptFetchOptions {
929    pub(crate) integrity_metadata: String,
930    #[no_trace]
931    pub(crate) credentials_mode: CredentialsMode,
932    pub(crate) cryptographic_nonce: String,
933    #[no_trace]
934    pub(crate) parser_metadata: ParserMetadata,
935    #[no_trace]
936    pub(crate) referrer_policy: ReferrerPolicy,
937    /// <https://html.spec.whatwg.org/multipage/#concept-script-fetch-options-render-blocking>
938    /// The boolean value of render-blocking used for the initial fetch and for fetching any imported modules.
939    /// Unless otherwise stated, its value is false.
940    pub(crate) render_blocking: bool,
941}
942
943impl ScriptFetchOptions {
944    /// <https://html.spec.whatwg.org/multipage/#default-classic-script-fetch-options>
945    pub(crate) fn default_classic_script() -> ScriptFetchOptions {
946        Self {
947            cryptographic_nonce: String::new(),
948            integrity_metadata: String::new(),
949            parser_metadata: ParserMetadata::NotParserInserted,
950            credentials_mode: CredentialsMode::CredentialsSameOrigin,
951            referrer_policy: ReferrerPolicy::EmptyString,
952            render_blocking: false,
953        }
954    }
955
956    /// <https://html.spec.whatwg.org/multipage/#descendant-script-fetch-options>
957    pub(crate) fn descendant_fetch_options(
958        &self,
959        url: &ServoUrl,
960        global: &GlobalScope,
961    ) -> ScriptFetchOptions {
962        // Step 2. Let integrity be the result of resolving a module integrity metadata with url and settingsObject.
963        let integrity = global.import_map().resolve_a_module_integrity_metadata(url);
964
965        // Step 1. Let newOptions be a copy of originalOptions.
966        // TODO Step 4. Set newOptions's fetch priority to "auto".
967        Self {
968            // Step 3. Set newOptions's integrity metadata to integrity.
969            integrity_metadata: integrity,
970            cryptographic_nonce: self.cryptographic_nonce.clone(),
971            credentials_mode: self.credentials_mode,
972            parser_metadata: self.parser_metadata,
973            referrer_policy: self.referrer_policy,
974            render_blocking: self.render_blocking,
975        }
976    }
977}
978
979#[expect(unsafe_code)]
980pub(crate) unsafe fn module_script_from_reference_private(
981    reference_private: &RawHandle<JSVal>,
982) -> Option<&ModuleScript> {
983    if reference_private.get().is_undefined() {
984        return None;
985    }
986    unsafe { (reference_private.get().to_private() as *const ModuleScript).as_ref() }
987}
988
989#[expect(unsafe_code)]
990#[expect(non_snake_case)]
991/// <https://tc39.es/ecma262/#sec-HostLoadImportedModule>
992/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
993unsafe extern "C" fn HostResolveImportedModule(
994    cx: *mut RawJSContext,
995    reference_private: RawHandleValue,
996    specifier: RawHandle<*mut JSObject>,
997) -> *mut JSObject {
998    // SAFETY: it is safe to construct a JSContext from engine hook.
999    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
1000    let mut realm = CurrentRealm::assert(&mut cx);
1001    let global_scope = GlobalScope::from_current_realm(&mut realm);
1002
1003    let cx = &mut realm;
1004
1005    // Step 5.
1006    let module_data = unsafe { module_script_from_reference_private(&reference_private) };
1007    let jsstr = unsafe { GetModuleRequestSpecifier(cx, Handle::from_raw(specifier)) };
1008    let module_type = unsafe { GetModuleRequestType(cx, Handle::from_raw(specifier)) };
1009
1010    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
1011    let url = ModuleTree::resolve_module_specifier(
1012        &global_scope,
1013        module_data,
1014        DOMString::from(specifier),
1015    );
1016
1017    // Step 6.
1018    assert!(url.is_ok());
1019
1020    let parsed_url = url.unwrap();
1021
1022    // Step 4 & 7.
1023    let module = global_scope.get_module_map_entry(&(parsed_url, module_type));
1024
1025    // Step 9.
1026    assert!(module.as_ref().is_some_and(
1027        |status| matches!(status, ModuleStatus::Loaded(module_tree) if module_tree.is_some())
1028    ));
1029
1030    let ModuleStatus::Loaded(Some(module_tree)) = module.unwrap() else {
1031        unreachable!()
1032    };
1033
1034    let fetched_module_object = module_tree.get_record();
1035
1036    // Step 8.
1037    assert!(fetched_module_object.is_some());
1038
1039    // Step 10.
1040    if let Some(record) = fetched_module_object {
1041        return record.handle().get();
1042    }
1043
1044    unreachable!()
1045}
1046
1047// https://searchfox.org/firefox-esr140/rev/3fccb0ec900b931a1a752b02eafab1fb9652d9b9/js/loader/ModuleLoaderBase.h#560
1048const SLOT_MODULEPRIVATE: usize = 0;
1049
1050#[expect(unsafe_code)]
1051#[expect(non_snake_case)]
1052/// <https://tc39.es/ecma262/#sec-hostgetimportmetaproperties>
1053/// <https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties>
1054unsafe extern "C" fn HostPopulateImportMeta(
1055    cx: *mut RawJSContext,
1056    reference_private: RawHandleValue,
1057    meta_object: RawHandle<*mut JSObject>,
1058) -> bool {
1059    // SAFETY: it is safe to construct a JSContext from engine hook.
1060    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
1061    let mut realm = CurrentRealm::assert(&mut cx);
1062    let global_scope = GlobalScope::from_current_realm(&mut realm);
1063
1064    // Step 2.
1065    let base_url = match unsafe { module_script_from_reference_private(&reference_private) } {
1066        Some(module_data) => module_data.base_url.clone(),
1067        None => global_scope.api_base_url(),
1068    };
1069
1070    unsafe {
1071        let url_string = JS_NewStringCopyN(
1072            &mut cx,
1073            base_url.as_str().as_ptr() as *const _,
1074            base_url.as_str().len(),
1075        );
1076        rooted!(&in(cx) let url_string = url_string);
1077
1078        // Step 3.
1079        if !JS_DefineProperty4(
1080            &mut cx,
1081            Handle::from_raw(meta_object),
1082            c"url".as_ptr(),
1083            url_string.handle(),
1084            JSPROP_ENUMERATE.into(),
1085        ) {
1086            return false;
1087        }
1088
1089        // Step 5. Let resolveFunction be ! CreateBuiltinFunction(steps, 1, "resolve", « »).
1090        let resolve_function = DefineFunctionWithReserved(
1091            &mut cx,
1092            meta_object.get(),
1093            c"resolve".as_ptr(),
1094            Some(import_meta_resolve),
1095            1,
1096            JSPROP_ENUMERATE.into(),
1097        );
1098
1099        rooted!(&in(cx) let obj = JS_GetFunctionObject(resolve_function));
1100        assert!(!obj.is_null());
1101        SetFunctionNativeReserved(
1102            obj.get(),
1103            SLOT_MODULEPRIVATE,
1104            &reference_private.get() as *const _,
1105        );
1106    }
1107
1108    true
1109}
1110
1111#[expect(unsafe_code)]
1112unsafe extern "C" fn import_meta_resolve(cx: *mut RawJSContext, argc: u32, vp: *mut JSVal) -> bool {
1113    // SAFETY: it is safe to construct a JSContext from engine hook.
1114    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1115    let mut realm = CurrentRealm::assert(&mut cx);
1116    let global_scope = GlobalScope::from_current_realm(&mut realm);
1117
1118    let cx = &mut realm;
1119
1120    let args = unsafe { CallArgs::from_vp(vp, argc) };
1121
1122    rooted!(&in(cx) let module_private = unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_MODULEPRIVATE) });
1123    let reference_private = module_private.handle().into();
1124    let module_data = unsafe { module_script_from_reference_private(&reference_private) };
1125
1126    // https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties
1127
1128    // Step 4.1. Set specifier to ? ToString(specifier).
1129    let specifier = unsafe {
1130        let value = HandleValue::from_raw(args.get(0));
1131
1132        match NonNull::new(ToString(cx, value)) {
1133            Some(jsstr) => jsstr_to_string(cx, jsstr).into(),
1134            None => return false,
1135        }
1136    };
1137
1138    // Step 4.2. Let url be the result of resolving a module specifier given moduleScript and specifier.
1139    let url = ModuleTree::resolve_module_specifier(&global_scope, module_data, specifier);
1140
1141    match url {
1142        Ok(url) => {
1143            // Step 4.3. Return the serialization of url.
1144            url.as_str()
1145                .safe_to_jsval(cx, unsafe { MutableHandleValue::from_raw(args.rval()) });
1146            true
1147        },
1148        Err(error) => {
1149            let resolution_error = gen_type_error(cx, &global_scope, error);
1150
1151            unsafe {
1152                JS_SetPendingException(
1153                    cx,
1154                    resolution_error.handle(),
1155                    ExceptionStackBehavior::Capture,
1156                );
1157            }
1158            false
1159        },
1160    }
1161}
1162
1163#[expect(clippy::too_many_arguments)]
1164/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree>
1165/// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet/module-worker-script-graph>
1166pub(crate) fn fetch_a_module_worker_script_graph(
1167    cx: &mut JSContext,
1168    global: &GlobalScope,
1169    url: ServoUrl,
1170    fetch_client: RequestClient,
1171    destination: Destination,
1172    referrer: Referrer,
1173    credentials_mode: CredentialsMode,
1174    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1175) {
1176    let global_scope = DomRoot::from_ref(global);
1177
1178    // Step 1. Let options be a script fetch options whose cryptographic nonce
1179    // is the empty string, integrity metadata is the empty string, parser
1180    // metadata is "not-parser-inserted", credentials mode is credentialsMode,
1181    // referrer policy is the empty string, and fetch priority is "auto".
1182    let options = ScriptFetchOptions {
1183        integrity_metadata: "".into(),
1184        credentials_mode,
1185        cryptographic_nonce: "".into(),
1186        parser_metadata: ParserMetadata::NotParserInserted,
1187        referrer_policy: ReferrerPolicy::EmptyString,
1188        render_blocking: false,
1189    };
1190
1191    // Step 2. Fetch a single module script given url, fetchClient, destination, options,
1192    // settingsObject, "client", true, and onSingleFetchComplete as defined below.
1193    fetch_a_single_module_script(
1194        cx,
1195        url,
1196        fetch_client.clone(),
1197        global,
1198        destination,
1199        options,
1200        referrer,
1201        None,
1202        true,
1203        Some(IntroductionType::WORKER),
1204        move |cx, module_tree| {
1205            let Some(module) = module_tree else {
1206                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1207                return on_complete(cx, None);
1208            };
1209
1210            // Step 1.2. Fetch the descendants of and link result given fetchClient, destination,
1211            // and onComplete.
1212            fetch_the_descendants_and_link_module_script(
1213                cx,
1214                &global_scope,
1215                module,
1216                fetch_client,
1217                destination,
1218                on_complete,
1219            );
1220        },
1221    );
1222}
1223
1224/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-script-tree>
1225pub(crate) fn fetch_an_external_module_script(
1226    cx: &mut JSContext,
1227    url: ServoUrl,
1228    global: &GlobalScope,
1229    options: ScriptFetchOptions,
1230    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1231) {
1232    let referrer = global.get_referrer();
1233    let fetch_client = global.request_client(Some(cx.no_gc()));
1234    let global_scope = DomRoot::from_ref(global);
1235
1236    // Step 1. Fetch a single module script given url, settingsObject, "script", options, settingsObject, "client", true,
1237    // and with the following steps given result:
1238    fetch_a_single_module_script(
1239        cx,
1240        url,
1241        fetch_client.clone(),
1242        global,
1243        Destination::Script,
1244        options,
1245        referrer,
1246        None,
1247        true,
1248        Some(IntroductionType::SRC_SCRIPT),
1249        move |cx, module_tree| {
1250            let Some(module) = module_tree else {
1251                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1252                return on_complete(cx, None);
1253            };
1254
1255            // Step 1.2. Fetch the descendants of and link result given settingsObject, "script", and onComplete.
1256            fetch_the_descendants_and_link_module_script(
1257                cx,
1258                &global_scope,
1259                module,
1260                fetch_client,
1261                Destination::Script,
1262                on_complete,
1263            );
1264        },
1265    );
1266}
1267
1268/// <https://html.spec.whatwg.org/multipage/#fetch-a-modulepreload-module-script-graph>
1269pub(crate) fn fetch_a_modulepreload_module(
1270    cx: &mut JSContext,
1271    url: ServoUrl,
1272    destination: Destination,
1273    global: &GlobalScope,
1274    options: ScriptFetchOptions,
1275    on_complete: impl FnOnce(&mut JSContext, bool) + 'static,
1276) {
1277    let referrer = global.get_referrer();
1278    let fetch_client = global.request_client(Some(cx.no_gc()));
1279    let global_scope = DomRoot::from_ref(global);
1280
1281    // Note: There is a specification inconsistency, `fetch_a_single_module_script` doesn't allow
1282    // fetching top level JSON/CSS module scripts, but should be possible when preloading.
1283    let module_type = if let Destination::Json = destination {
1284        Some(ModuleType::JSON)
1285    } else {
1286        None
1287    };
1288
1289    // Step 1. Fetch a single module script given url, settingsObject, destination, options, settingsObject,
1290    // "client", true, and with the following steps given result:
1291    fetch_a_single_module_script(
1292        cx,
1293        url,
1294        fetch_client.clone(),
1295        global,
1296        destination,
1297        options,
1298        referrer,
1299        module_type,
1300        true,
1301        Some(IntroductionType::SRC_SCRIPT),
1302        move |cx, result| {
1303            // Step 1. Run onComplete given result.
1304            on_complete(cx, result.is_none());
1305
1306            // Step 2. Assert: settingsObject's global object implements Window.
1307            assert!(global_scope.is::<Window>());
1308
1309            // Step 3. If result is not null, optionally fetch the descendants of and link result
1310            // given settingsObject, destination, and an empty algorithm.
1311            if pref!(dom_allow_preloading_module_descendants) &&
1312                let Some(module) = result
1313            {
1314                fetch_the_descendants_and_link_module_script(
1315                    cx,
1316                    &global_scope,
1317                    module,
1318                    fetch_client,
1319                    destination,
1320                    |_, _| {},
1321                );
1322            }
1323        },
1324    );
1325}
1326
1327#[expect(clippy::too_many_arguments)]
1328/// <https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph>
1329pub(crate) fn fetch_inline_module_script(
1330    cx: &mut JSContext,
1331    global: &GlobalScope,
1332    module_script_text: Cow<'_, str>,
1333    url: ServoUrl,
1334    options: ScriptFetchOptions,
1335    line_number: u32,
1336    introduction_type: Option<&'static CStr>,
1337    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1338) {
1339    // Step 1. Let script be the result of creating a JavaScript module script using sourceText, settingsObject, baseURL, and options.
1340    let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
1341        cx,
1342        module_script_text,
1343        global,
1344        &url,
1345        options,
1346        false,
1347        line_number,
1348        introduction_type,
1349    ));
1350    let fetch_client = global.request_client(Some(cx.no_gc()));
1351
1352    // Step 2. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
1353    fetch_the_descendants_and_link_module_script(
1354        cx,
1355        global,
1356        module_tree,
1357        fetch_client,
1358        Destination::Script,
1359        on_complete,
1360    );
1361}
1362
1363#[expect(unsafe_code)]
1364/// <https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script>
1365fn fetch_the_descendants_and_link_module_script(
1366    cx: &mut JSContext,
1367    global: &GlobalScope,
1368    module_script: Rc<ModuleTree>,
1369    fetch_client: RequestClient,
1370    destination: Destination,
1371    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1372) {
1373    // Step 1. Let record be moduleScript's record.
1374    // Step 2. If record is null, then:
1375    if module_script.get_record().is_none() {
1376        let parse_error = module_script.get_parse_error().cloned();
1377
1378        // Step 2.1. Set moduleScript's error to rethrow to moduleScript's parse error.
1379        module_script.set_rethrow_error(parse_error.unwrap());
1380
1381        // Step 2.2. Run onComplete given moduleScript.
1382        on_complete(cx, Some(module_script));
1383
1384        // Step 2.3. Return.
1385        return;
1386    }
1387
1388    // Step 3. Let state be Record
1389    // { [[ErrorToRethrow]]: null, [[Destination]]: destination, [[PerformFetch]]: null, [[FetchClient]]: fetchClient }.
1390    let state = Rc::new(LoadState {
1391        error_to_rethrow: RefCell::new(None),
1392        destination,
1393        fetch_client,
1394    });
1395
1396    // TODO Step 4. If performFetch was given, set state.[[PerformFetch]] to performFetch.
1397
1398    let mut realm = enter_auto_realm(cx, global);
1399    let cx = &mut realm.current_realm();
1400
1401    // Step 5. Let loadingPromise be record.LoadRequestedModules(state).
1402    let loading_promise = load_requested_modules(cx, module_script.clone(), Some(state.clone()));
1403
1404    let global_scope = DomRoot::from_ref(global);
1405    let fulfilled_module = module_script.clone();
1406    let fulfilled_on_complete = on_complete.clone();
1407
1408    // Step 6. Upon fulfillment of loadingPromise, run the following steps:
1409    let loading_promise_fulfillment = ModuleHandler::new_boxed(Box::new(
1410        task!(fulfilled_steps: |cx, global_scope: DomRoot<GlobalScope>| {
1411            let mut realm = AutoRealm::new(
1412                cx,
1413                NonNull::new(global_scope.reflector().get_jsobject().get()).unwrap(),
1414            );
1415            let cx = &mut *realm;
1416
1417            let handle = fulfilled_module.get_record().map(|module| module.handle()).unwrap();
1418
1419            // Step 6.1. Perform record.Link().
1420            let link = unsafe { ModuleLink(cx, handle) };
1421
1422            // If this throws an exception, catch it, and set moduleScript's error to rethrow to that exception.
1423            if !link {
1424                let exception = RethrowError::from_pending_exception(cx);
1425                fulfilled_module.set_rethrow_error(exception);
1426            }
1427
1428            // Step 6.2. Run onComplete given moduleScript.
1429            fulfilled_on_complete(cx, Some(fulfilled_module));
1430        }),
1431    ));
1432
1433    // Step 7. Upon rejection of loadingPromise, run the following steps:
1434    let loading_promise_rejection =
1435        ModuleHandler::new_boxed(Box::new(task!(rejected_steps: |cx, state: Rc<LoadState>| {
1436            // Step 7.1. If state.[[ErrorToRethrow]] is not null, set moduleScript's error to rethrow to state.[[ErrorToRethrow]]
1437            // and run onComplete given moduleScript.
1438            if let Some(error) = state.error_to_rethrow.borrow().as_ref() {
1439                module_script.set_rethrow_error(error.clone());
1440                on_complete(cx, Some(module_script));
1441            } else {
1442                // Step 7.2. Otherwise, run onComplete given null.
1443                on_complete(cx, None);
1444            }
1445        })));
1446
1447    let handler = PromiseNativeHandler::new(
1448        cx,
1449        global,
1450        Some(loading_promise_fulfillment),
1451        Some(loading_promise_rejection),
1452    );
1453
1454    run_a_callback::<DomTypeHolder, _>(global, || {
1455        loading_promise.append_native_handler(cx, &handler);
1456    });
1457}
1458
1459/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
1460#[expect(clippy::too_many_arguments)]
1461pub(crate) fn fetch_a_single_module_script(
1462    cx: &mut JSContext,
1463    url: ServoUrl,
1464    fetch_client: RequestClient,
1465    global: &GlobalScope,
1466    destination: Destination,
1467    options: ScriptFetchOptions,
1468    referrer: Referrer,
1469    module_type: Option<ModuleType>,
1470    is_top_level: bool,
1471    introduction_type: Option<&'static CStr>,
1472    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
1473) {
1474    // Step 1. Let moduleType be "javascript-or-wasm".
1475    // Step 2. If moduleRequest was given, then set moduleType to the result of running the
1476    // module type from module request steps given moduleRequest.
1477    let module_type = module_type.unwrap_or(ModuleType::JavaScript);
1478
1479    // TODO Step 3. Assert: the result of running the module type allowed steps given moduleType and settingsObject is true.
1480    // Otherwise, we would not have reached this point because a failure would have been raised
1481    // when inspecting moduleRequest.[[Attributes]] in HostLoadImportedModule or fetch a single imported module script.
1482
1483    // Step 4. Let moduleMap be settingsObject's module map.
1484    let module_request = (url.clone(), module_type);
1485    let entry = global.get_module_map_entry(&module_request);
1486
1487    let pending = match entry {
1488        Some(ModuleStatus::Fetching(pending)) => pending,
1489        // Step 6. If moduleMap[(url, moduleType)] exists, run onComplete given moduleMap[(url, moduleType)], and return.
1490        Some(ModuleStatus::Loaded(module_tree)) => {
1491            return on_complete(cx, module_tree);
1492        },
1493        None => DomRefCell::new(None),
1494    };
1495
1496    let global_scope = DomRoot::from_ref(global);
1497    let module_map_key = module_request.clone();
1498    let handler = ModuleHandler::new_boxed(Box::new(
1499        task!(fetch_completed: |cx, global_scope: DomRoot<GlobalScope>| {
1500            let key = module_map_key;
1501            let module = global_scope.get_module_map_entry(&key);
1502
1503            if let Some(ModuleStatus::Loaded(module_tree)) = module {
1504                on_complete(cx, module_tree);
1505            }
1506        }),
1507    ));
1508
1509    let handler = PromiseNativeHandler::new(cx, global, Some(handler), None);
1510
1511    let mut realm = enter_auto_realm(cx, global);
1512    let cx = &mut realm.current_realm();
1513
1514    run_a_callback::<DomTypeHolder, _>(global, || {
1515        let has_pending_fetch = pending.borrow().is_some();
1516
1517        let promise = Promise::new_in_realm(cx);
1518
1519        // Step 5. If moduleMap[(url, moduleType)] is "fetching", wait in parallel until that entry's value changes,
1520        // then queue a task on the networking task source to proceed with running the following steps.
1521        if has_pending_fetch {
1522            promise.append_native_handler(cx, &handler);
1523
1524            // Append an handler to the existing pending fetch, once resolved it will queue a task
1525            // to run onComplete.
1526            let continue_loading_handler = PromiseNativeHandler::new(
1527                cx,
1528                global,
1529                Some(Box::new(QueueTaskHandler { promise })),
1530                None,
1531            );
1532
1533            // be careful of a borrow hazard here (do not hold a RefCell over a possible GC pause)
1534            let pending_promise = pending.borrow_mut().take();
1535            if let Some(promise) = pending_promise {
1536                promise.append_native_handler(cx, &continue_loading_handler);
1537                let _ = pending.borrow_mut().insert(promise);
1538            }
1539            return;
1540        }
1541
1542        promise.append_native_handler(cx, &handler);
1543
1544        let prev = pending.borrow_mut().replace(promise);
1545        assert!(prev.is_none());
1546
1547        // Step 7. Set moduleMap[(url, moduleType)] to "fetching".
1548        global.set_module_map(module_request.clone(), ModuleStatus::Fetching(pending));
1549
1550        // We only need a policy container when fetching the root of a module worker.
1551        let policy_container = (is_top_level && global.is::<WorkerGlobalScope>())
1552            .then(|| fetch_client.policy_container.clone());
1553
1554        // Step 8. Let request be a new request whose URL is url, mode is "cors", referrer is referrer, and client is fetchClient.
1555
1556        // Step 10. If destination is "worker", "sharedworker", or "serviceworker", and isTopLevel is true,
1557        // then set request's mode to "same-origin".
1558        let mode = match destination {
1559            Destination::Worker | Destination::SharedWorker if is_top_level => {
1560                RequestMode::SameOrigin
1561            },
1562            _ => RequestMode::CorsMode,
1563        };
1564
1565        // Step 9. Set request's destination to the result of running the
1566        // fetch destination from module type steps given destination and moduleType.
1567        let destination = match module_type {
1568            ModuleType::JSON => Destination::Json,
1569            ModuleType::JavaScript | ModuleType::Unknown => destination,
1570        };
1571
1572        // TODO Step 11. Set request's initiator type to "script".
1573
1574        // Step 12. Set up the module script request given request and options.
1575        let request = RequestBuilder::new(
1576            global.webview_id(),
1577            ensure_blob_referenced_by_url_is_kept_alive(global, url.clone()),
1578            referrer,
1579        )
1580        .destination(destination)
1581        .parser_metadata(options.parser_metadata)
1582        .integrity_metadata(options.integrity_metadata.clone())
1583        .credentials_mode(options.credentials_mode)
1584        .referrer_policy(options.referrer_policy)
1585        .mode(mode)
1586        .cryptographic_nonce_metadata(options.cryptographic_nonce.clone())
1587        .client(fetch_client)
1588        .pipeline_id(Some(global.pipeline_id()));
1589
1590        let context = ModuleContext {
1591            owner: Trusted::new(global),
1592            data: vec![],
1593            metadata: None,
1594            module_request,
1595            options,
1596            status: Ok(()),
1597            introduction_type,
1598            policy_container,
1599        };
1600
1601        let task_source = global.task_manager().networking_task_source().to_sendable();
1602        global.fetch(request, context, task_source);
1603    })
1604}
1605
1606pub(crate) type ModuleSpecifierMap = IndexMap<String, Option<ServoUrl>>;
1607pub(crate) type ModuleIntegrityMap = IndexMap<ServoUrl, String>;
1608
1609/// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record>
1610#[derive(Default, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
1611pub(crate) struct ResolvedModule {
1612    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-serialized-base-url>
1613    base_url: String,
1614    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-specifier>
1615    specifier: String,
1616    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-as-url>
1617    #[no_trace]
1618    specifier_url: Option<ServoUrl>,
1619}
1620
1621impl ResolvedModule {
1622    pub(crate) fn new(
1623        base_url: String,
1624        specifier: String,
1625        specifier_url: Option<ServoUrl>,
1626    ) -> Self {
1627        Self {
1628            base_url,
1629            specifier,
1630            specifier_url,
1631        }
1632    }
1633}
1634
1635/// <https://html.spec.whatwg.org/multipage/#import-map-processing-model>
1636#[derive(Default, JSTraceable, MallocSizeOf)]
1637pub(crate) struct ImportMap {
1638    #[no_trace]
1639    imports: ModuleSpecifierMap,
1640    #[no_trace]
1641    scopes: IndexMap<ServoUrl, ModuleSpecifierMap>,
1642    #[no_trace]
1643    integrity: ModuleIntegrityMap,
1644}
1645
1646impl ImportMap {
1647    /// <https://html.spec.whatwg.org/multipage/#resolving-a-module-integrity-metadata>
1648    pub(crate) fn resolve_a_module_integrity_metadata(&self, url: &ServoUrl) -> String {
1649        // Step 1. Let map be settingsObject's global object's import map.
1650
1651        // Step 2. If map's integrity[url] does not exist, then return the empty string.
1652        // Step 3. Return map's integrity[url].
1653        self.integrity.get(url).cloned().unwrap_or_default()
1654    }
1655}
1656
1657/// <https://html.spec.whatwg.org/multipage/#register-an-import-map>
1658pub(crate) fn register_import_map(
1659    cx: &mut JSContext,
1660    global: &GlobalScope,
1661    result: Fallible<ImportMap>,
1662) {
1663    match result {
1664        Ok(new_import_map) => {
1665            // Step 2. Merge existing and new import maps, given global and result's import map.
1666            merge_existing_and_new_import_maps(cx, global, new_import_map);
1667        },
1668        Err(exception) => {
1669            let mut realm = enter_auto_realm(cx, global);
1670            let cx = &mut realm.current_realm();
1671
1672            // Step 1. If result's error to rethrow is not null, then report
1673            // an exception given by result's error to rethrow for global and return.
1674            throw_dom_exception(cx, global, exception);
1675            report_pending_exception(cx);
1676        },
1677    }
1678}
1679
1680/// <https://html.spec.whatwg.org/multipage/#merge-existing-and-new-import-maps>
1681fn merge_existing_and_new_import_maps(
1682    cx: &mut JSContext,
1683    global: &GlobalScope,
1684    new_import_map: ImportMap,
1685) {
1686    // Step 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
1687    let new_import_map_scopes = new_import_map.scopes;
1688
1689    // Step 2. Let oldImportMap be global's import map.
1690    let mut old_import_map = global.import_map_mut();
1691
1692    // Step 3. Let newImportMapImports be a deep copy of newImportMap's imports.
1693    let mut new_import_map_imports = new_import_map.imports;
1694
1695    let resolved_module_set = global.resolved_module_set();
1696    // Step 4. For each scopePrefix → scopeImports of newImportMapScopes:
1697    for (scope_prefix, mut scope_imports) in new_import_map_scopes {
1698        // Step 4.1. For each record of global's resolved module set:
1699        for record in resolved_module_set.iter() {
1700            // If scopePrefix is record's serialized base URL, or if scopePrefix ends with
1701            // U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then:
1702            let prefix = scope_prefix.as_str();
1703            if prefix == record.base_url ||
1704                (record.base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
1705            {
1706                // For each specifierKey → resolutionResult of scopeImports:
1707                scope_imports.retain(|key, val| {
1708                    // If specifierKey is record's specifier, or if all of the following conditions are true:
1709                    // specifierKey ends with U+002F (/);
1710                    // specifierKey is a code unit prefix of record's specifier;
1711                    // either record's specifier as a URL is null or is special,
1712                    if *key == record.specifier ||
1713                        (key.ends_with('\u{002f}') &&
1714                            record.specifier.starts_with(key) &&
1715                            (record.specifier_url.is_none() ||
1716                                record
1717                                    .specifier_url
1718                                    .as_ref()
1719                                    .is_some_and(|u| u.is_special_scheme())))
1720                    {
1721                        // The user agent may report a warning to the console indicating the ignored rule.
1722                        // They may choose to avoid reporting if the rule is identical to an existing one.
1723                        Console::internal_warn(
1724                            cx,
1725                            global,
1726                            format!("Ignored rule: {key} -> {val:?}."),
1727                        );
1728                        // Remove scopeImports[specifierKey].
1729                        false
1730                    } else {
1731                        true
1732                    }
1733                })
1734            }
1735        }
1736
1737        // Step 4.2 If scopePrefix exists in oldImportMap's scopes
1738        if old_import_map.scopes.contains_key(&scope_prefix) {
1739            // set oldImportMap's scopes[scopePrefix] to the result of
1740            // merging module specifier maps, given scopeImports and oldImportMap's scopes[scopePrefix].
1741            let merged_module_specifier_map = merge_module_specifier_maps(
1742                cx,
1743                global,
1744                scope_imports,
1745                &old_import_map.scopes[&scope_prefix],
1746            );
1747            old_import_map
1748                .scopes
1749                .insert(scope_prefix, merged_module_specifier_map);
1750        } else {
1751            // Step 4.3 Otherwise, set oldImportMap's scopes[scopePrefix] to scopeImports.
1752            old_import_map.scopes.insert(scope_prefix, scope_imports);
1753        }
1754    }
1755
1756    // Step 5. For each url → integrity of newImportMap's integrity:
1757    for (url, integrity) in &new_import_map.integrity {
1758        // Step 5.1 If url exists in oldImportMap's integrity, then:
1759        if old_import_map.integrity.contains_key(url) {
1760            // Step 5.1.1 The user agent may report a warning to the console indicating the ignored rule.
1761            // They may choose to avoid reporting if the rule is identical to an existing one.
1762            Console::internal_warn(cx, global, format!("Ignored rule: {url} -> {integrity}."));
1763            // Step 5.1.2 Continue.
1764            continue;
1765        }
1766
1767        // Step 5.2 Set oldImportMap's integrity[url] to integrity.
1768        old_import_map
1769            .integrity
1770            .insert(url.clone(), integrity.clone());
1771    }
1772
1773    // Step 6. For each record of global's resolved module set:
1774    for record in resolved_module_set.iter() {
1775        // For each specifier → url of newImportMapImports:
1776        new_import_map_imports.retain(|specifier, val| {
1777            // If specifier starts with record's specifier, then:
1778            //
1779            // Note: Spec is wrong, we need to check if record's specifier starts with specifier
1780            // See: https://github.com/whatwg/html/issues/11875
1781            if record.specifier.starts_with(specifier) {
1782                // The user agent may report a warning to the console indicating the ignored rule.
1783                // They may choose to avoid reporting if the rule is identical to an existing one.
1784                Console::internal_warn(
1785                    cx,
1786                    global,
1787                    format!("Ignored rule: {specifier} -> {val:?}."),
1788                );
1789                // Remove newImportMapImports[specifier].
1790                false
1791            } else {
1792                true
1793            }
1794        });
1795    }
1796
1797    // Step 7. Set oldImportMap's imports to the result of merge module specifier maps,
1798    // given newImportMapImports and oldImportMap's imports.
1799    let merged_module_specifier_map =
1800        merge_module_specifier_maps(cx, global, new_import_map_imports, &old_import_map.imports);
1801    old_import_map.imports = merged_module_specifier_map;
1802
1803    // https://html.spec.whatwg.org/multipage/#the-resolution-algorithm
1804    // Sort scopes to ensure entries are visited from most-specific to least-specific.
1805    old_import_map
1806        .scopes
1807        .sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
1808}
1809
1810/// <https://html.spec.whatwg.org/multipage/#merge-module-specifier-maps>
1811fn merge_module_specifier_maps(
1812    cx: &mut JSContext,
1813    global: &GlobalScope,
1814    new_map: ModuleSpecifierMap,
1815    old_map: &ModuleSpecifierMap,
1816) -> ModuleSpecifierMap {
1817    // Step 1. Let mergedMap be a deep copy of oldMap.
1818    let mut merged_map = old_map.clone();
1819
1820    // Step 2. For each specifier → url of newMap:
1821    for (specifier, url) in new_map {
1822        // Step 2.1 If specifier exists in oldMap, then:
1823        if old_map.contains_key(&specifier) {
1824            // Step 2.1.1 The user agent may report a warning to the console indicating the ignored rule.
1825            // They may choose to avoid reporting if the rule is identical to an existing one.
1826            Console::internal_warn(cx, global, format!("Ignored rule: {specifier} -> {url:?}."));
1827
1828            // Step 2.1.2 Continue.
1829            continue;
1830        }
1831
1832        // Step 2.2 Set mergedMap[specifier] to url.
1833        merged_map.insert(specifier, url);
1834    }
1835
1836    merged_map
1837}
1838
1839/// <https://html.spec.whatwg.org/multipage/#parse-an-import-map-string>
1840pub(crate) fn parse_an_import_map_string(
1841    cx: &mut JSContext,
1842    global: &GlobalScope,
1843    input: &str,
1844    base_url: ServoUrl,
1845) -> Fallible<ImportMap> {
1846    // Step 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
1847    let parsed: JsonValue = serde_json::from_str(input)
1848        .map_err(|_| Error::Type(c"The value needs to be a JSON object.".to_owned()))?;
1849    // Step 2. If parsed is not an ordered map, then throw a TypeError indicating that the
1850    // top-level value needs to be a JSON object.
1851    let JsonValue::Object(mut parsed) = parsed else {
1852        return Err(Error::Type(
1853            c"The top-level value needs to be a JSON object.".to_owned(),
1854        ));
1855    };
1856
1857    // Step 3. Let sortedAndNormalizedImports be an empty ordered map.
1858    let mut sorted_and_normalized_imports = ModuleSpecifierMap::new();
1859    // Step 4. If parsed["imports"] exists, then:
1860    if let Some(imports) = parsed.get("imports") {
1861        // Step 4.1 If parsed["imports"] is not an ordered map, then throw a TypeError
1862        // indicating that the value for the "imports" top-level key needs to be a JSON object.
1863        let JsonValue::Object(imports) = imports else {
1864            return Err(Error::Type(
1865                c"The \"imports\" top-level value needs to be a JSON object.".to_owned(),
1866            ));
1867        };
1868        // Step 4.2 Set sortedAndNormalizedImports to the result of sorting and
1869        // normalizing a module specifier map given parsed["imports"] and baseURL.
1870        sorted_and_normalized_imports =
1871            sort_and_normalize_module_specifier_map(cx, global, imports, &base_url);
1872    }
1873
1874    // Step 5. Let sortedAndNormalizedScopes be an empty ordered map.
1875    let mut sorted_and_normalized_scopes: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
1876    // Step 6. If parsed["scopes"] exists, then:
1877    if let Some(scopes) = parsed.get("scopes") {
1878        // Step 6.1 If parsed["scopes"] is not an ordered map, then throw a TypeError
1879        // indicating that the value for the "scopes" top-level key needs to be a JSON object.
1880        let JsonValue::Object(scopes) = scopes else {
1881            return Err(Error::Type(
1882                c"The \"scopes\" top-level value needs to be a JSON object.".to_owned(),
1883            ));
1884        };
1885        // Step 6.2 Set sortedAndNormalizedScopes to the result of sorting and
1886        // normalizing scopes given parsed["scopes"] and baseURL.
1887        sorted_and_normalized_scopes = sort_and_normalize_scopes(cx, global, scopes, &base_url)?;
1888    }
1889
1890    // Step 7. Let normalizedIntegrity be an empty ordered map.
1891    let mut normalized_integrity = ModuleIntegrityMap::new();
1892    // Step 8. If parsed["integrity"] exists, then:
1893    if let Some(integrity) = parsed.get("integrity") {
1894        // Step 8.1 If parsed["integrity"] is not an ordered map, then throw a TypeError
1895        // indicating that the value for the "integrity" top-level key needs to be a JSON object.
1896        let JsonValue::Object(integrity) = integrity else {
1897            return Err(Error::Type(
1898                c"The \"integrity\" top-level value needs to be a JSON object.".to_owned(),
1899            ));
1900        };
1901        // Step 8.2 Set normalizedIntegrity to the result of normalizing
1902        // a module integrity map given parsed["integrity"] and baseURL.
1903        normalized_integrity = normalize_module_integrity_map(cx, global, integrity, &base_url);
1904    }
1905
1906    // Step 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity",
1907    // then the user agent should report a warning to the console indicating that an invalid
1908    // top-level key was present in the import map.
1909    parsed.retain(|k, _| !matches!(k.as_str(), "imports" | "scopes" | "integrity"));
1910    if !parsed.is_empty() {
1911        Console::internal_warn(
1912            cx,
1913            global,
1914            "Invalid top-level key was present in the import map.
1915                Only \"imports\", \"scopes\", and \"integrity\" are allowed."
1916                .to_string(),
1917        );
1918    }
1919
1920    // Step 10. Return an import map
1921    Ok(ImportMap {
1922        imports: sorted_and_normalized_imports,
1923        scopes: sorted_and_normalized_scopes,
1924        integrity: normalized_integrity,
1925    })
1926}
1927
1928/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-a-module-specifier-map>
1929fn sort_and_normalize_module_specifier_map(
1930    cx: &mut JSContext,
1931    global: &GlobalScope,
1932    original_map: &JsonMap<String, JsonValue>,
1933    base_url: &ServoUrl,
1934) -> ModuleSpecifierMap {
1935    // Step 1. Let normalized be an empty ordered map.
1936    let mut normalized = ModuleSpecifierMap::new();
1937
1938    // Step 2. For each specifier_key -> value in originalMap
1939    for (specifier_key, value) in original_map {
1940        // Step 2.1 Let normalized_specifier_key be the result of
1941        // normalizing a specifier key given specifier_key and base_url.
1942        let Some(normalized_specifier_key) =
1943            normalize_specifier_key(cx, global, specifier_key, base_url)
1944        else {
1945            // Step 2.2 If normalized_specifier_key is null, then continue.
1946            continue;
1947        };
1948
1949        // Step 2.3 If value is not a string, then:
1950        let JsonValue::String(value) = value else {
1951            // Step 2.3.1 The user agent may report a warning to the console
1952            // indicating that addresses need to be strings.
1953            Console::internal_warn(cx, global, "Addresses need to be strings.".to_string());
1954
1955            // Step 2.3.2 Set normalized[normalized_specifier_key] to null.
1956            normalized.insert(normalized_specifier_key, None);
1957            // Step 2.3.3 Continue.
1958            continue;
1959        };
1960
1961        // Step 2.4. Let address_url be the result of resolving a URL-like module specifier given value and baseURL.
1962        let Some(address_url) =
1963            ModuleTree::resolve_url_like_module_specifier(value.as_str(), base_url)
1964        else {
1965            // Step 2.5 If address_url is null, then:
1966            // Step 2.5.1. The user agent may report a warning to the console
1967            // indicating that the address was invalid.
1968            Console::internal_warn(
1969                cx,
1970                global,
1971                format!("Value failed to resolve to module specifier: {value}"),
1972            );
1973
1974            // Step 2.5.2 Set normalized[normalized_specifier_key] to null.
1975            normalized.insert(normalized_specifier_key, None);
1976            // Step 2.5.3 Continue.
1977            continue;
1978        };
1979
1980        // Step 2.6 If specifier_key ends with U+002F (/), and the serialization of
1981        // address_url does not end with U+002F (/), then:
1982        if specifier_key.ends_with('\u{002f}') && !address_url.as_str().ends_with('\u{002f}') {
1983            // step 2.6.1. The user agent may report a warning to the console
1984            // indicating that an invalid address was given for the specifier key specifierKey;
1985            // since specifierKey ends with a slash, the address needs to as well.
1986            Console::internal_warn(
1987                cx,
1988                global,
1989                format!(
1990                    "Invalid address for specifier key '{specifier_key}': {address_url}.
1991                    Since specifierKey ends with a slash, the address needs to as well."
1992                ),
1993            );
1994
1995            // Step 2.6.2 Set normalized[normalized_specifier_key] to null.
1996            normalized.insert(normalized_specifier_key, None);
1997            // Step 2.6.3 Continue.
1998            continue;
1999        }
2000
2001        // Step 2.7 Set normalized[normalized_specifier_key] to address_url.
2002        normalized.insert(normalized_specifier_key, Some(address_url));
2003    }
2004
2005    // Step 3. Return the result of sorting in descending order normalized
2006    // with an entry a being less than an entry b if a's key is code unit less than b's key.
2007    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
2008    normalized
2009}
2010
2011/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-scopes>
2012fn sort_and_normalize_scopes(
2013    cx: &mut JSContext,
2014    global: &GlobalScope,
2015    original_map: &JsonMap<String, JsonValue>,
2016    base_url: &ServoUrl,
2017) -> Fallible<IndexMap<ServoUrl, ModuleSpecifierMap>> {
2018    // Step 1. Let normalized be an empty ordered map.
2019    let mut normalized: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
2020
2021    // Step 2. For each scopePrefix → potentialSpecifierMap of originalMap:
2022    for (scope_prefix, potential_specifier_map) in original_map {
2023        // Step 2.1 If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating
2024        // that the value of the scope with prefix scopePrefix needs to be a JSON object.
2025        let JsonValue::Object(potential_specifier_map) = potential_specifier_map else {
2026            return Err(Error::Type(
2027                c"The value of the scope with prefix scopePrefix needs to be a JSON object."
2028                    .to_owned(),
2029            ));
2030        };
2031
2032        // Step 2.2 Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
2033        let Ok(scope_prefix_url) = ServoUrl::parse_with_base(Some(base_url), scope_prefix) else {
2034            // Step 2.3 If scopePrefixURL is failure, then:
2035            // Step 2.3.1 The user agent may report a warning
2036            // to the console that the scope prefix URL was not parseable.
2037            Console::internal_warn(
2038                cx,
2039                global,
2040                format!("Scope prefix URL was not parseable: {scope_prefix}"),
2041            );
2042            // Step 2.3.2 Continue.
2043            continue;
2044        };
2045
2046        // Step 2.4 Let normalizedScopePrefix be the serialization of scopePrefixURL.
2047        let normalized_scope_prefix = scope_prefix_url;
2048
2049        // Step 2.5 Set normalized[normalizedScopePrefix] to the result of sorting and
2050        // normalizing a module specifier map given potentialSpecifierMap and baseURL.
2051        let normalized_specifier_map =
2052            sort_and_normalize_module_specifier_map(cx, global, potential_specifier_map, base_url);
2053        normalized.insert(normalized_scope_prefix, normalized_specifier_map);
2054    }
2055
2056    // Step 3. Return the result of sorting in descending order normalized,
2057    // with an entry a being less than an entry b if a's key is code unit less than b's key.
2058    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
2059    Ok(normalized)
2060}
2061
2062/// <https://html.spec.whatwg.org/multipage/#normalizing-a-module-integrity-map>
2063fn normalize_module_integrity_map(
2064    cx: &mut JSContext,
2065    global: &GlobalScope,
2066    original_map: &JsonMap<String, JsonValue>,
2067    base_url: &ServoUrl,
2068) -> ModuleIntegrityMap {
2069    // Step 1. Let normalized be an empty ordered map.
2070    let mut normalized = ModuleIntegrityMap::new();
2071
2072    // Step 2. For each key → value of originalMap:
2073    for (key, value) in original_map {
2074        // Step 2.1 Let resolvedURL be the result of
2075        // resolving a URL-like module specifier given key and baseURL.
2076        let Some(resolved_url) =
2077            ModuleTree::resolve_url_like_module_specifier(key.as_str(), base_url)
2078        else {
2079            // Step 2.2 If resolvedURL is null, then:
2080            // Step 2.2.1 The user agent may report a warning
2081            // to the console indicating that the key failed to resolve.
2082            Console::internal_warn(
2083                cx,
2084                global,
2085                format!("Key failed to resolve to module specifier: {key}"),
2086            );
2087            // Step 2.2.2 Continue.
2088            continue;
2089        };
2090
2091        // Step 2.3 If value is not a string, then:
2092        let JsonValue::String(value) = value else {
2093            // Step 2.3.1 The user agent may report a warning
2094            // to the console indicating that integrity metadata values need to be strings.
2095            Console::internal_warn(
2096                cx,
2097                global,
2098                "Integrity metadata values need to be strings.".to_string(),
2099            );
2100            // Step 2.3.2 Continue.
2101            continue;
2102        };
2103
2104        // Step 2.4 Set normalized[resolvedURL] to value.
2105        normalized.insert(resolved_url, value.clone());
2106    }
2107
2108    // Step 3. Return normalized.
2109    normalized
2110}
2111
2112/// <https://html.spec.whatwg.org/multipage/#normalizing-a-specifier-key>
2113fn normalize_specifier_key(
2114    cx: &mut JSContext,
2115    global: &GlobalScope,
2116    specifier_key: &str,
2117    base_url: &ServoUrl,
2118) -> Option<String> {
2119    // step 1. If specifierKey is the empty string, then:
2120    if specifier_key.is_empty() {
2121        // step 1.1 The user agent may report a warning to the console
2122        // indicating that specifier keys may not be the empty string.
2123        Console::internal_warn(
2124            cx,
2125            global,
2126            "Specifier keys may not be the empty string.".to_string(),
2127        );
2128        // step 1.2 Return null.
2129        return None;
2130    }
2131    // step 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
2132    let url = ModuleTree::resolve_url_like_module_specifier(specifier_key, base_url);
2133
2134    // step 3. If url is not null, then return the serialization of url.
2135    if let Some(url) = url {
2136        return Some(url.into_string());
2137    }
2138
2139    // step 4. Return specifierKey.
2140    Some(specifier_key.to_string())
2141}
2142
2143/// <https://html.spec.whatwg.org/multipage/#resolving-an-imports-match>
2144///
2145/// When the error is thrown, it will terminate the entire resolve a module specifier algorithm
2146/// without any further fallbacks.
2147fn resolve_imports_match(
2148    normalized_specifier: &str,
2149    as_url: Option<&ServoUrl>,
2150    specifier_map: &ModuleSpecifierMap,
2151) -> Fallible<Option<ServoUrl>> {
2152    // Step 1. For each specifierKey → resolutionResult of specifierMap:
2153    for (specifier_key, resolution_result) in specifier_map {
2154        // Step 1.1 If specifierKey is normalizedSpecifier, then:
2155        if specifier_key == normalized_specifier {
2156            if let Some(resolution_result) = resolution_result {
2157                // Step 1.1.2 Assert: resolutionResult is a URL.
2158                // This is checked by Url type already.
2159                // Step 1.1.3 Return resolutionResult.
2160                return Ok(Some(resolution_result.clone()));
2161            } else {
2162                // Step 1.1.1 If resolutionResult is null, then throw a TypeError.
2163                return Err(Error::Type(
2164                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
2165                ));
2166            }
2167        }
2168
2169        // Step 1.2 If all of the following are true:
2170        // - specifierKey ends with U+002F (/)
2171        // - specifierKey is a code unit prefix of normalizedSpecifier
2172        // - either asURL is null, or asURL is special, then:
2173        if specifier_key.ends_with('\u{002f}') &&
2174            normalized_specifier.starts_with(specifier_key) &&
2175            (as_url.is_none() || as_url.is_some_and(|u| u.is_special_scheme()))
2176        {
2177            // Step 1.2.1 If resolutionResult is null, then throw a TypeError.
2178            // Step 1.2.2 Assert: resolutionResult is a URL.
2179            let Some(resolution_result) = resolution_result else {
2180                return Err(Error::Type(
2181                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
2182                ));
2183            };
2184
2185            // Step 1.2.3 Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.
2186            let after_prefix = normalized_specifier
2187                .strip_prefix(specifier_key)
2188                .expect("specifier_key should be the prefix of normalized_specifier");
2189
2190            // Step 1.2.4 Assert: resolutionResult, serialized, ends with U+002F (/), as enforced during parsing.
2191            debug_assert!(resolution_result.as_str().ends_with('\u{002f}'));
2192
2193            // Step 1.2.5 Let url be the result of URL parsing afterPrefix with resolutionResult.
2194            let url = ServoUrl::parse_with_base(Some(resolution_result), after_prefix);
2195
2196            // Step 1.2.6 If url is failure, then throw a TypeError
2197            // Step 1.2.7 Assert: url is a URL.
2198            let Ok(url) = url else {
2199                return Err(Error::Type(
2200                    c"Resolution of normalizedSpecifier was blocked since
2201                    the afterPrefix portion could not be URL-parsed relative to
2202                    the resolutionResult mapped to by the specifierKey prefix."
2203                        .to_owned(),
2204                ));
2205            };
2206
2207            // Step 1.2.8 If the serialization of resolutionResult is not
2208            // a code unit prefix of the serialization of url, then throw a TypeError
2209            if !url.as_str().starts_with(resolution_result.as_str()) {
2210                return Err(Error::Type(
2211                    c"Resolution of normalizedSpecifier was blocked due to
2212                    it backtracking above its prefix specifierKey."
2213                        .to_owned(),
2214                ));
2215            }
2216
2217            // Step 1.2.9 Return url.
2218            return Ok(Some(url));
2219        }
2220    }
2221
2222    // Step 2. Return null.
2223    Ok(None)
2224}