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