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