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