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