Skip to main content

script/
script_module.rs

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