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