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