Skip to main content

script/modules/
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::borrow::Cow;
9use std::cell::{OnceCell, RefCell};
10use std::collections::hash_map::Entry;
11use std::ffi::CStr;
12use std::fmt::Debug;
13use std::ptr::NonNull;
14use std::rc::Rc;
15use std::{mem, ptr};
16
17use bytes::{Bytes, BytesMut};
18use encoding_rs::UTF_8;
19use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
20use hyper_serde::Serde;
21use js::context::JSContext;
22use js::conversions::{ToJSValConvertible, jsstr_to_string};
23use js::gc::{HandleObject, MutableHandleValue};
24use js::jsapi::{
25    CallArgs, ColumnNumberOneOrigin, ExceptionStackBehavior, GetFunctionNativeReserved,
26    GetModuleLoadHook, Handle as RawHandle, HandleValue as RawHandleValue, Heap,
27    JS_GetFunctionObject, JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE, JSRuntime,
28    JSScript, ModuleErrorBehaviour, ModuleType, SetFunctionNativeReserved, SetModuleLoadHook,
29    SetModuleMetadataHook, SetModulePrivate, SetScriptPrivateReferenceHooks, Value,
30};
31use js::jsval::{JSVal, ObjectValue, PrivateValue, UndefinedValue};
32use js::realm::{AutoRealm, CurrentRealm};
33use js::rust::wrappers2::{
34    CompileJsonModule1, CompileModule1, DefineFunctionWithReserved, GetModuleRequestSpecifier,
35    JS_ClearPendingException, JS_DefineProperty4, JS_GetModulePrivate, JS_GetPendingException,
36    JS_NewStringCopyN, JS_SetPendingException, ModuleEvaluate, ThrowOnModuleEvaluationFailure,
37};
38use js::rust::{Handle, HandleValue, ToString, transform_str_to_source_text};
39use mime::Mime;
40use net_traits::blob_url_store::UrlWithBlobClaim;
41use net_traits::http_status::HttpStatus;
42use net_traits::mime_classifier::MimeClassifier;
43use net_traits::policy_container::PolicyContainer;
44use net_traits::request::{
45    CredentialsMode, Destination, ParserMetadata, Referrer, RequestBuilder, RequestClient,
46    RequestId, RequestMode,
47};
48use net_traits::{FetchMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming};
49use script_bindings::cell::DomRefCell;
50use script_bindings::error::Fallible;
51use script_bindings::reflector::DomObject;
52use script_bindings::trace::CustomTraceable;
53use servo_config::pref;
54use servo_url::ServoUrl;
55
56use crate::dom::bindings::error::{Error, ErrorToJsval, report_pending_exception};
57use crate::dom::bindings::inheritance::Castable;
58use crate::dom::bindings::refcounted::Trusted;
59use crate::dom::bindings::root::DomRoot;
60use crate::dom::bindings::trace::RootedTraceableBox;
61use crate::dom::csp::{GlobalCspReporting, Violation};
62use crate::dom::globalscope::GlobalScope;
63use crate::dom::globalscope::script_execution::fill_compile_options;
64use crate::dom::html::htmlscriptelement::{SCRIPT_JS_MIMES, substitute_with_local_script};
65use crate::dom::performance::performanceresourcetiming::InitiatorType;
66use crate::dom::promisenativehandler::Callback;
67use crate::dom::script_execution::ScriptOptions;
68use crate::dom::types::{
69    DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, WorkerGlobalScope, WorkletGlobalScope,
70};
71use crate::dom::window::Window;
72use crate::fetch::network_listener::{self, FetchResponseListener, ResourceTimingListener};
73use crate::modules::import_map::{ModuleSpecifierMap, resolve_url_like_module_specifier};
74use crate::modules::module_loading::{
75    LoadState, host_load_imported_module, load_requested_modules,
76};
77use crate::realms::enter_auto_realm;
78use crate::runtime::script_runtime::IntroductionType;
79use crate::tasks::task::NonSendTaskBox;
80use crate::unminify::{ScriptSource, unminify_js};
81
82pub(crate) fn gen_type_error(
83    cx: &mut JSContext,
84    global: &GlobalScope,
85    error: Error,
86) -> RethrowError {
87    rooted!(&in(cx) let mut thrown = UndefinedValue());
88    error.to_jsval(cx, global, thrown.handle_mut());
89
90    RethrowError(RootedTraceableBox::from_box(Heap::boxed(thrown.get())))
91}
92
93#[derive(JSTraceable)]
94pub(crate) struct ModuleObject(RootedTraceableBox<Heap<*mut JSObject>>);
95
96impl ModuleObject {
97    pub(crate) fn new(obj: HandleObject) -> ModuleObject {
98        ModuleObject(RootedTraceableBox::from_box(Heap::boxed(obj.get())))
99    }
100
101    pub(crate) fn handle(&'_ self) -> HandleObject<'_> {
102        self.0.handle()
103    }
104}
105
106#[derive(JSTraceable)]
107pub(crate) struct RethrowError(RootedTraceableBox<Heap<JSVal>>);
108
109impl RethrowError {
110    pub(crate) fn new(val: Box<Heap<JSVal>>) -> Self {
111        Self(RootedTraceableBox::from_box(val))
112    }
113
114    #[expect(unsafe_code)]
115    pub(crate) fn from_pending_exception(cx: &mut JSContext) -> Self {
116        rooted!(&in(cx) let mut exception = UndefinedValue());
117        assert!(unsafe { JS_GetPendingException(cx, exception.handle_mut()) });
118        unsafe { JS_ClearPendingException(cx) };
119
120        Self::new(Heap::boxed(exception.get()))
121    }
122
123    pub(crate) fn handle(&self) -> Handle<'_, JSVal> {
124        self.0.handle()
125    }
126}
127
128impl Debug for RethrowError {
129    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
130        "RethrowError(...)".fmt(fmt)
131    }
132}
133
134impl Clone for RethrowError {
135    fn clone(&self) -> Self {
136        Self(RootedTraceableBox::from_box(Heap::boxed(self.0.get())))
137    }
138}
139
140pub(crate) struct ModuleScript {
141    pub(crate) base_url: ServoUrl,
142    pub(crate) options: ScriptFetchOptions,
143    pub(crate) owner: Option<Trusted<GlobalScope>>,
144}
145
146impl ModuleScript {
147    pub(crate) fn new(
148        base_url: ServoUrl,
149        options: ScriptFetchOptions,
150        owner: Option<Trusted<GlobalScope>>,
151    ) -> Self {
152        ModuleScript {
153            base_url,
154            options,
155            owner,
156        }
157    }
158}
159
160pub(crate) type ModuleRequest = (ServoUrl, ModuleType);
161
162#[derive(JSTraceable)]
163pub(crate) enum ModuleStatus {
164    #[expect(clippy::type_complexity)]
165    Fetching(#[no_trace] Vec<Box<dyn FnOnce(&mut JSContext, Option<Rc<ModuleTree>>)>>),
166    Loaded(Rc<ModuleTree>),
167}
168
169#[derive(JSTraceable, MallocSizeOf)]
170pub(crate) struct ModuleTree {
171    #[ignore_malloc_size_of = "mozjs"]
172    record: OnceCell<ModuleObject>,
173    #[ignore_malloc_size_of = "mozjs"]
174    parse_error: OnceCell<RethrowError>,
175    #[ignore_malloc_size_of = "mozjs"]
176    rethrow_error: DomRefCell<Option<RethrowError>>,
177}
178
179impl ModuleTree {
180    pub(crate) fn get_record(&self) -> Option<&ModuleObject> {
181        self.record.get()
182    }
183
184    pub(crate) fn get_parse_error(&self) -> Option<&RethrowError> {
185        self.parse_error.get()
186    }
187
188    pub(crate) fn get_rethrow_error(&self) -> &DomRefCell<Option<RethrowError>> {
189        &self.rethrow_error
190    }
191
192    pub(crate) fn set_rethrow_error(&self, rethrow_error: RethrowError) {
193        *self.rethrow_error.borrow_mut() = Some(rethrow_error);
194    }
195}
196
197impl ModuleTree {
198    #[expect(unsafe_code)]
199    #[expect(clippy::too_many_arguments)]
200    /// <https://html.spec.whatwg.org/multipage/#creating-a-javascript-module-script>
201    fn create_a_javascript_module_script(
202        cx: &mut JSContext,
203        source: Cow<'_, str>,
204        global: &GlobalScope,
205        url: &ServoUrl,
206        options: ScriptFetchOptions,
207        external: bool,
208        line_number: u32,
209        introduction_type: Option<&'static CStr>,
210    ) -> Self {
211        let mut realm = AutoRealm::new(
212            cx,
213            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
214        );
215        let cx = &mut *realm;
216
217        let owner = Trusted::new(global);
218
219        // Step 2. Let script be a new module script that this algorithm will subsequently initialize.
220        // Step 6. Set script's parse error and error to rethrow to null.
221        let module = ModuleTree {
222            record: OnceCell::new(),
223            parse_error: OnceCell::new(),
224            rethrow_error: DomRefCell::new(None),
225        };
226
227        let compile_options = fill_compile_options(
228            cx,
229            url.as_str(),
230            ScriptOptions::empty(),
231            introduction_type,
232            line_number,
233        );
234
235        let mut source = if let Some(unminified_js_dir) = global.unminified_js_dir() {
236            let mut module_source = ScriptSource {
237                source,
238                external,
239                url,
240            };
241            unminify_js(&mut module_source, unminified_js_dir);
242            transform_str_to_source_text(&module_source.source)
243        } else {
244            transform_str_to_source_text(&source)
245        };
246
247        unsafe {
248            // Step 7. Let result be ParseModule(source, settings's realm, script).
249            rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
250            module_script.set(CompileModule1(cx, compile_options.ptr, &mut source));
251
252            // Step 8. If result is a list of errors, then:
253            if module_script.is_null() {
254                warn!("fail to compile module script of {}", url);
255
256                // Step 8.1. Set script's parse error to result[0].
257                let _ = module
258                    .parse_error
259                    .set(RethrowError::from_pending_exception(cx));
260
261                // Step 8.2. Return script.
262                return module;
263            }
264
265            // Step 3. Set script's settings object to settings.
266            // Step 4. Set script's base URL to baseURL.
267            // Step 5. Set script's fetch options to options.
268            let module_script_data = Rc::new(ModuleScript::new(url.clone(), options, Some(owner)));
269
270            SetModulePrivate(
271                module_script.get(),
272                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
273            );
274
275            // Step 9. Set script's record to result.
276            let _ = module.record.set(ModuleObject::new(module_script.handle()));
277        }
278
279        // Step 10. Return script.
280        module
281    }
282
283    #[expect(unsafe_code)]
284    /// <https://html.spec.whatwg.org/multipage/#creating-a-json-module-script>
285    fn create_a_json_module_script(
286        cx: &mut JSContext,
287        source: &str,
288        global: &GlobalScope,
289        url: &ServoUrl,
290        introduction_type: Option<&'static CStr>,
291    ) -> Self {
292        let mut realm = AutoRealm::new(
293            cx,
294            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
295        );
296        let cx = &mut *realm;
297
298        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
299        // Step 4. Set script's parse error and error to rethrow to null.
300        let module = ModuleTree {
301            record: OnceCell::new(),
302            parse_error: OnceCell::new(),
303            rethrow_error: DomRefCell::new(None),
304        };
305
306        // Step 2. Set script's settings object to settings.
307        // Step 3. Set script's base URL and fetch options to null.
308        // Note: We don't need to call `SetModulePrivate` for json scripts
309
310        let compile_options = fill_compile_options(
311            cx,
312            url.as_str(),
313            ScriptOptions::empty(),
314            introduction_type,
315            1, // line_number
316        );
317
318        rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
319
320        unsafe {
321            // Step 5. Let result be ParseJSONModule(source).
322            module_script.set(CompileJsonModule1(
323                cx,
324                compile_options.ptr,
325                &mut transform_str_to_source_text(source),
326            ));
327        }
328
329        // If this throws an exception, catch it, and set script's parse error to that exception, and return script.
330        if module_script.is_null() {
331            warn!("fail to compile module script of {}", url);
332
333            let _ = module
334                .parse_error
335                .set(RethrowError::from_pending_exception(cx));
336            return module;
337        }
338
339        // Step 6. Set script's record to result.
340        let _ = module.record.set(ModuleObject::new(module_script.handle()));
341
342        // Step 7. Return script.
343        module
344    }
345
346    /// Execute the provided module, storing the evaluation return value in the provided
347    /// mutable handle.
348    #[expect(unsafe_code)]
349    pub(crate) fn execute_module(
350        &self,
351        cx: &mut JSContext,
352        global: &GlobalScope,
353        module_record: HandleObject,
354        mut eval_result: MutableHandleValue,
355    ) -> Result<(), RethrowError> {
356        let mut realm = AutoRealm::new(
357            cx,
358            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
359        );
360        let cx = &mut *realm;
361
362        unsafe {
363            let ok = ModuleEvaluate(cx, module_record, eval_result.reborrow());
364            assert!(ok, "module evaluation failed");
365
366            rooted!(&in(cx) let mut evaluation_promise = ptr::null_mut::<JSObject>());
367            if eval_result.is_object() {
368                evaluation_promise.set(eval_result.to_object());
369            }
370
371            let throw_result = ThrowOnModuleEvaluationFailure(
372                cx,
373                evaluation_promise.handle(),
374                ModuleErrorBehaviour::ThrowModuleErrorsSync,
375            );
376            if !throw_result {
377                warn!("fail to evaluate module");
378
379                Err(RethrowError::from_pending_exception(cx))
380            } else {
381                debug!("module evaluated successfully");
382                Ok(())
383            }
384        }
385    }
386
387    #[expect(unsafe_code)]
388    pub(crate) fn report_error(&self, cx: &mut JSContext, global: &GlobalScope) {
389        let module_error = self.rethrow_error.borrow();
390
391        if let Some(exception) = &*module_error {
392            let mut realm = enter_auto_realm(cx, global);
393            let cx = &mut realm.current_realm();
394
395            unsafe {
396                JS_SetPendingException(cx, exception.handle(), ExceptionStackBehavior::Capture);
397            }
398            report_pending_exception(cx);
399        }
400    }
401
402    /// <https://html.spec.whatwg.org/multipage/#resolve-a-module-specifier>
403    pub(crate) fn resolve_module_specifier(
404        global: &GlobalScope,
405        script: Option<&ModuleScript>,
406        specifier: String,
407    ) -> Fallible<ServoUrl> {
408        // Step 1~3 to get settingsObject and baseURL
409        let script_global = script.and_then(|s| s.owner.as_ref().map(|o| o.root()));
410        // Step 1. Let settingsObject and baseURL be null.
411        let (global, base_url): (&GlobalScope, &ServoUrl) = match script {
412            // Step 2. If referringScript is not null, then:
413            // Set settingsObject to referringScript's settings object.
414            // Set baseURL to referringScript's base URL.
415            Some(s) => (script_global.as_ref().map_or(global, |g| g), &s.base_url),
416            // Step 3. Otherwise:
417            // Set settingsObject to the current settings object.
418            // Set baseURL to settingsObject's API base URL.
419            // FIXME(#37553): Is this the correct current settings object?
420            None => (global, &global.api_base_url()),
421        };
422
423        // Step 4. Let importMap be an empty import map.
424        // Step 5. If settingsObject's global object implements Window, then set importMap to settingsObject's
425        // global object's import map.
426        let import_map = if global.is::<Window>() {
427            Some(global.import_map())
428        } else {
429            None
430        };
431
432        // Step 6. Let serializedBaseURL be baseURL, serialized.
433        let serialized_base_url = base_url.as_str();
434        // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
435        let as_url = resolve_url_like_module_specifier(&specifier, base_url);
436        // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null;
437        // otherwise, specifier.
438        let normalized_specifier = match &as_url {
439            Some(url) => url.as_str(),
440            None => &specifier,
441        };
442
443        // Step 9. Let result be a URL-or-null, initially null.
444        let mut result = None;
445        if let Some(map) = import_map {
446            // Step 10. For each scopePrefix → scopeImports of importMap's scopes:
447            for (prefix, imports) in &map.scopes {
448                // Step 10.1 If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/)
449                // and scopePrefix is a code unit prefix of serializedBaseURL, then:
450                let prefix = prefix.as_str();
451                if prefix == serialized_base_url ||
452                    (serialized_base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
453                {
454                    // Step 10.1.1 Let scopeImportsMatch be the result of resolving an imports match
455                    // given normalizedSpecifier, asURL, and scopeImports.
456                    let scope_imports_match =
457                        resolve_imports_match(normalized_specifier, as_url.as_ref(), imports)?;
458
459                    // Step 10.1.2 If scopeImportsMatch is not null, then set result to scopeImportsMatch, and break.
460                    if scope_imports_match.is_some() {
461                        result = scope_imports_match;
462                        break;
463                    }
464                }
465            }
466
467            // Step 11. If result is null, set result to the result of resolving an imports match given
468            // normalizedSpecifier, asURL, and importMap's imports.
469            if result.is_none() {
470                result =
471                    resolve_imports_match(normalized_specifier, as_url.as_ref(), &map.imports)?;
472            }
473        }
474
475        // Step 12. If result is null, set it to asURL.
476        if result.is_none() {
477            result = as_url.clone();
478        }
479
480        // Step 13. If result is not null, then:
481        match result {
482            Some(result) => {
483                // Step 13.1 Add module to resolved module set given settingsObject, serializedBaseURL,
484                // normalizedSpecifier, and asURL.
485                global.add_module_to_resolved_module_set(
486                    serialized_base_url,
487                    normalized_specifier,
488                    as_url.clone(),
489                );
490                // Step 13.2 Return result.
491                Ok(result)
492            },
493            // Step 14. Throw a TypeError indicating that specifier was a bare specifier,
494            // but was not remapped to anything by importMap.
495            None => Err(Error::Type(
496                c"Specifier was a bare specifier, but was not remapped to anything by importMap."
497                    .to_owned(),
498            )),
499        }
500    }
501}
502
503#[derive(JSTraceable, MallocSizeOf)]
504pub(crate) struct ModuleHandler {
505    #[ignore_malloc_size_of = "Measuring trait objects is hard"]
506    task: DomRefCell<Option<Box<dyn NonSendTaskBox>>>,
507}
508
509impl ModuleHandler {
510    pub(crate) fn new_boxed(task: Box<dyn NonSendTaskBox>) -> Box<dyn Callback> {
511        Box::new(Self {
512            task: DomRefCell::new(Some(task)),
513        })
514    }
515}
516
517impl Callback for ModuleHandler {
518    fn callback(&self, cx: &mut CurrentRealm, _v: HandleValue) {
519        let task = self.task.borrow_mut().take().unwrap();
520        task.run_box(cx);
521    }
522}
523
524/// The context required for asynchronously loading an external module script source.
525struct ModuleContext {
526    /// The owner of the module that initiated the request.
527    owner: Trusted<GlobalScope>,
528    /// The response body received to date.
529    data: BytesMut,
530    /// The response metadata received to date.
531    metadata: Option<Metadata>,
532    /// Url and type of the requested module.
533    module_request: ModuleRequest,
534    /// Options for the current script fetch
535    options: ScriptFetchOptions,
536    /// Indicates whether the request failed, and why
537    status: Result<(), NetworkError>,
538    /// `introductionType` value to set in the `CompileOptionsWrapper`.
539    introduction_type: Option<&'static CStr>,
540    /// <https://html.spec.whatwg.org/multipage/#policy-container>
541    policy_container: Option<PolicyContainer>,
542}
543
544impl FetchResponseListener for ModuleContext {
545    // TODO(cybai): Perhaps add custom steps to perform fetch here?
546    fn process_request_body(&mut self, _: RequestId) {}
547
548    fn process_response(
549        &mut self,
550        _: &mut js::context::JSContext,
551        _: RequestId,
552        metadata: Result<FetchMetadata, NetworkError>,
553    ) {
554        self.metadata = metadata.ok().map(|meta| match meta {
555            FetchMetadata::Unfiltered(m) => m,
556            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
557        });
558
559        let status = self
560            .metadata
561            .as_ref()
562            .map(|m| m.status.clone())
563            .unwrap_or_else(HttpStatus::new_error);
564
565        self.status = {
566            if status.is_error() {
567                Err(NetworkError::ResourceLoadError(
568                    "No http status code received".to_owned(),
569                ))
570            } else if status.is_success() {
571                Ok(())
572            } else {
573                Err(NetworkError::ResourceLoadError(format!(
574                    "HTTP error code {}",
575                    status.code()
576                )))
577            }
578        };
579    }
580
581    fn process_response_chunk(
582        &mut self,
583        _: &mut js::context::JSContext,
584        _: RequestId,
585        chunk: Bytes,
586    ) {
587        if self.status.is_ok() {
588            self.data.extend_from_slice(&chunk);
589        }
590    }
591
592    /// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
593    /// Step 13
594    fn process_response_eof(
595        mut self,
596        cx: &mut js::context::JSContext,
597        _: RequestId,
598        response: Result<(), NetworkError>,
599        timing: ResourceFetchTiming,
600    ) {
601        let global = self.owner.root();
602        let (_url, module_type) = &self.module_request;
603
604        if !global.is::<WorkletGlobalScope>() {
605            network_listener::submit_timing(cx, &self, &response, &timing);
606        }
607
608        let module_map = global.module_map();
609
610        // Step 1. If any of the following are true: bodyBytes is null or failure; or response's
611        // status is not an ok status, then:
612        if let (Err(error), _) | (_, Err(error)) = (response.as_ref(), self.status.as_ref()) {
613            error!("Fetching module script failed {:?}", error);
614            // Step 1.1. Let callbacks be moduleMap[(url, moduleType)].
615            // Step 1.2. Remove moduleMap[(url, moduleType)].
616            let Some(ModuleStatus::Fetching(callbacks)) =
617                module_map.safe_borrow_mut(cx).remove(&self.module_request)
618            else {
619                return error!("Processing response for a non pending module request");
620            };
621
622            // Step 1.3. For each callback of callbacks: run callback given null.
623            for callback in callbacks {
624                (callback)(cx, None);
625            }
626
627            // Step 1.4. Return.
628            return;
629        }
630
631        let metadata = self.metadata.take().unwrap();
632
633        // The processResponseConsumeBody steps defined inside
634        // [run a worker](https://html.spec.whatwg.org/multipage/#run-a-worker)
635        if let Some(policy_container) = self.policy_container {
636            let workerscope = global.downcast::<WorkerGlobalScope>().expect(
637                "We only need a policy container when initializing a worker's globalscope.",
638            );
639            workerscope.process_response_for_workerscope(&metadata, &policy_container);
640        }
641
642        let final_url = metadata.final_url;
643
644        // Step 2. Let mimeType be the result of extracting a MIME type from response's header list.
645        let mime_type: Option<Mime> = metadata.content_type.map(Serde::into_inner).map(Into::into);
646
647        // Step 3. Let moduleScript be null.
648        let mut module_script = None;
649
650        // Step 4. Let referrerPolicy be the result of parsing the `Referrer-Policy` header given response. [REFERRERPOLICY]
651        let referrer_policy = metadata
652            .headers
653            .and_then(|headers| headers.typed_get::<ReferrerPolicyHeader>())
654            .into();
655
656        // Step 5. If referrerPolicy is not the empty string, set options's referrer policy to referrerPolicy.
657        if referrer_policy != ReferrerPolicy::EmptyString {
658            self.options.referrer_policy = referrer_policy;
659        }
660
661        // TODO Step 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set
662        // moduleScript to the result of creating a WebAssembly module script given bodyBytes, settingsObject, response's URL, and options.
663
664        // TODO handle CSS module scripts on the next mozjs ESR bump.
665
666        if let Some(mime) = mime_type {
667            // Step 7.1 Let sourceText be the result of UTF-8 decoding bodyBytes.
668            let (mut source_text, _) = UTF_8.decode_with_bom_removal(&self.data);
669
670            // Step 7.2 If mimeType is a JavaScript MIME type and moduleType is "javascript-or-wasm", then set moduleScript
671            // to the result of creating a JavaScript module script given sourceText, settingsObject, response's URL, and options.
672            if SCRIPT_JS_MIMES.contains(&mime.essence_str()) &&
673                matches!(module_type, ModuleType::JavaScript)
674            {
675                if let Some(window) = global.downcast::<Window>() &&
676                    let Some(script_souce) = window.local_script_source()
677                {
678                    substitute_with_local_script(script_souce, &mut source_text, &final_url);
679                }
680
681                let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
682                    cx,
683                    source_text,
684                    &global,
685                    &final_url,
686                    self.options,
687                    true,
688                    1,
689                    self.introduction_type,
690                ));
691                module_script = Some(module_tree);
692            } else if MimeClassifier::is_json(&mime) && matches!(module_type, ModuleType::JSON) {
693                // Step 7.4 If mimeType is a JSON MIME type and moduleType is "json",
694                // then set moduleScript to the result of creating a JSON module script given sourceText and settingsObject.
695                let module_tree = Rc::new(ModuleTree::create_a_json_module_script(
696                    cx,
697                    &source_text,
698                    &global,
699                    &final_url,
700                    self.introduction_type,
701                ));
702                module_script = Some(module_tree);
703            }
704        }
705
706        let callbacks = match module_map
707            .safe_borrow_mut(cx)
708            .entry(self.module_request.clone())
709        {
710            Entry::Occupied(mut entry) => {
711                // Step 9. If moduleScript is null, then remove moduleMap[(url, moduleType)];
712                // otherwise set moduleMap[(url, moduleType)] to moduleScript.
713                let old_value = match module_script.as_ref() {
714                    None => entry.remove(),
715                    Some(module_script) => {
716                        entry.insert(ModuleStatus::Loaded(module_script.clone()))
717                    },
718                };
719
720                match old_value {
721                    ModuleStatus::Loaded(_) => {
722                        return error!("Processing response for a non pending module request");
723                    },
724                    ModuleStatus::Fetching(callbacks) => callbacks,
725                }
726            },
727            Entry::Vacant(_) => {
728                return error!("Processing response for a non pending module request");
729            },
730        };
731
732        // Step 10. For each callback of callbacks: run callback given moduleScript.
733        for callback in callbacks {
734            (callback)(cx, module_script.clone());
735        }
736    }
737
738    fn process_csp_violations(
739        &mut self,
740        cx: &mut js::context::JSContext,
741        _request_id: RequestId,
742        violations: Vec<Violation>,
743    ) {
744        let global = self.owner.root();
745        if let Some(scope) = global.downcast::<DedicatedWorkerGlobalScope>() {
746            scope.report_csp_violations(violations);
747        } else if let Some(scope) = global.downcast::<SharedWorkerGlobalScope>() {
748            scope.report_csp_violations(violations);
749        } else {
750            global.report_csp_violations(cx, violations, None, None);
751        }
752    }
753
754    fn process_content_length(&mut self, _request_id: RequestId, size: usize) {
755        self.data.reserve(size - self.data.len());
756    }
757}
758
759impl ResourceTimingListener for ModuleContext {
760    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
761        let initiator_type = InitiatorType::LocalName("module".to_string());
762        let (url, _) = &self.module_request;
763        (initiator_type, url.clone())
764    }
765
766    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
767        self.owner.root()
768    }
769}
770
771#[expect(unsafe_code)]
772#[expect(non_snake_case)]
773/// A function to register module hooks (e.g. listening on resolving modules,
774/// getting module metadata, getting script private reference and resolving dynamic import)
775pub(crate) unsafe fn EnsureModuleHooksInitialized(rt: *mut JSRuntime) {
776    unsafe {
777        if GetModuleLoadHook(rt).is_some() {
778            return;
779        }
780
781        SetModuleLoadHook(rt, Some(HostLoadImportedModule));
782        SetModuleMetadataHook(rt, Some(HostPopulateImportMeta));
783        SetScriptPrivateReferenceHooks(
784            rt,
785            Some(host_add_ref_top_level_script),
786            Some(host_release_top_level_script),
787        );
788    }
789}
790
791#[expect(unsafe_code)]
792unsafe extern "C" fn host_add_ref_top_level_script(value: *const Value) {
793    let val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
794    mem::forget(val.clone());
795    mem::forget(val);
796}
797
798#[expect(unsafe_code)]
799unsafe extern "C" fn host_release_top_level_script(value: *const Value) {
800    let _val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
801}
802
803#[derive(Clone, Debug, JSTraceable, MallocSizeOf)]
804/// <https://html.spec.whatwg.org/multipage/#script-fetch-options>
805pub(crate) struct ScriptFetchOptions {
806    pub(crate) integrity_metadata: String,
807    #[no_trace]
808    pub(crate) credentials_mode: CredentialsMode,
809    pub(crate) cryptographic_nonce: String,
810    #[no_trace]
811    pub(crate) parser_metadata: ParserMetadata,
812    #[no_trace]
813    pub(crate) referrer_policy: ReferrerPolicy,
814    /// <https://html.spec.whatwg.org/multipage/#concept-script-fetch-options-render-blocking>
815    /// The boolean value of render-blocking used for the initial fetch and for fetching any imported modules.
816    /// Unless otherwise stated, its value is false.
817    pub(crate) render_blocking: bool,
818}
819
820impl ScriptFetchOptions {
821    /// <https://html.spec.whatwg.org/multipage/#default-classic-script-fetch-options>
822    pub(crate) fn default_classic_script() -> ScriptFetchOptions {
823        Self {
824            cryptographic_nonce: String::new(),
825            integrity_metadata: String::new(),
826            parser_metadata: ParserMetadata::NotParserInserted,
827            credentials_mode: CredentialsMode::CredentialsSameOrigin,
828            referrer_policy: ReferrerPolicy::EmptyString,
829            render_blocking: false,
830        }
831    }
832
833    /// <https://html.spec.whatwg.org/multipage/#descendant-script-fetch-options>
834    pub(crate) fn descendant_fetch_options(
835        &self,
836        url: &ServoUrl,
837        global: &GlobalScope,
838    ) -> ScriptFetchOptions {
839        // Step 2. Let integrity be the result of resolving a module integrity metadata with url and settingsObject.
840        let integrity = global.import_map().resolve_a_module_integrity_metadata(url);
841
842        // Step 1. Let newOptions be a copy of originalOptions.
843        // TODO Step 4. Set newOptions's fetch priority to "auto".
844        Self {
845            // Step 3. Set newOptions's integrity metadata to integrity.
846            integrity_metadata: integrity,
847            cryptographic_nonce: self.cryptographic_nonce.clone(),
848            credentials_mode: self.credentials_mode,
849            parser_metadata: self.parser_metadata,
850            referrer_policy: self.referrer_policy,
851            render_blocking: self.render_blocking,
852        }
853    }
854}
855
856#[expect(unsafe_code)]
857pub(crate) unsafe fn module_script_from_reference_private<'a>(
858    reference_private: Handle<'a, JSVal>,
859) -> Option<&'a ModuleScript> {
860    if reference_private.get().is_undefined() {
861        return None;
862    }
863    unsafe { (reference_private.get().to_private() as *const ModuleScript).as_ref() }
864}
865
866#[expect(unsafe_code)]
867#[expect(non_snake_case)]
868/// <https://tc39.es/ecma262/#sec-HostLoadImportedModule>
869/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
870unsafe extern "C" fn HostLoadImportedModule(
871    cx: *mut RawJSContext,
872    referrer: RawHandle<*mut JSScript>,
873    module_request: RawHandle<*mut JSObject>,
874    host_defined: RawHandleValue,
875    payload: RawHandleValue,
876    _line_number: u32,
877    _column_number: ColumnNumberOneOrigin,
878) -> bool {
879    // SAFETY: it is safe to construct a JSContext from engine hook.
880    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
881    let mut realm = CurrentRealm::assert(&mut cx);
882    let cx = &mut realm;
883
884    let referrer = unsafe { Handle::from_raw(referrer) };
885    let module_request = unsafe { Handle::from_raw(module_request) };
886    let host_defined = unsafe { Handle::from_raw(host_defined) };
887    let payload = unsafe { Handle::from_raw(payload) };
888
889    let jsstr = unsafe { GetModuleRequestSpecifier(cx, module_request) };
890    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
891
892    host_load_imported_module(
893        cx,
894        referrer,
895        module_request,
896        specifier,
897        host_defined,
898        payload,
899    );
900    true
901}
902
903/// <https://searchfox.org/firefox-main/rev/9a8a80db6ce10ffc2fc91a1e25685eed59ce3501/js/loader/ModuleLoaderBase.h#597>
904const MODULE_RECORD_SLOT: usize = 0;
905
906#[expect(unsafe_code)]
907#[expect(non_snake_case)]
908/// <https://tc39.es/ecma262/#sec-hostgetimportmetaproperties>
909/// <https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties>
910unsafe extern "C" fn HostPopulateImportMeta(
911    cx: *mut RawJSContext,
912    module_record: RawHandle<*mut JSObject>,
913    meta_object: RawHandle<*mut JSObject>,
914) -> bool {
915    // SAFETY: it is safe to construct a JSContext from engine hook.
916    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
917    let mut realm = CurrentRealm::assert(&mut cx);
918    let global_scope = GlobalScope::from_current_realm(&mut realm);
919
920    // Step 2.
921    rooted!(&in(cx) let mut module_private: JSVal);
922    unsafe { JS_GetModulePrivate(module_record.get(), module_private.handle_mut()) };
923    let base_url = match unsafe { module_script_from_reference_private(module_private.handle()) } {
924        Some(module_data) => module_data.base_url.clone(),
925        None => global_scope.api_base_url(),
926    };
927
928    unsafe {
929        let url_string = JS_NewStringCopyN(
930            &mut cx,
931            base_url.as_str().as_ptr() as *const _,
932            base_url.as_str().len(),
933        );
934        rooted!(&in(cx) let url_string = url_string);
935
936        // Step 3.
937        if !JS_DefineProperty4(
938            &mut cx,
939            Handle::from_raw(meta_object),
940            c"url".as_ptr(),
941            url_string.handle(),
942            JSPROP_ENUMERATE.into(),
943        ) {
944            return false;
945        }
946
947        // Step 5. Let resolveFunction be ! CreateBuiltinFunction(steps, 1, "resolve", « »).
948        let resolve_function = DefineFunctionWithReserved(
949            &mut cx,
950            meta_object.get(),
951            c"resolve".as_ptr(),
952            Some(import_meta_resolve),
953            1,
954            JSPROP_ENUMERATE.into(),
955        );
956
957        if resolve_function.is_null() {
958            return false;
959        }
960
961        rooted!(&in(cx) let obj = JS_GetFunctionObject(resolve_function));
962        assert!(!obj.is_null());
963        SetFunctionNativeReserved(
964            obj.get(),
965            MODULE_RECORD_SLOT,
966            &ObjectValue(module_record.get()),
967        );
968    }
969
970    true
971}
972
973#[expect(unsafe_code)]
974unsafe extern "C" fn import_meta_resolve(cx: *mut RawJSContext, argc: u32, vp: *mut JSVal) -> bool {
975    // SAFETY: it is safe to construct a JSContext from engine hook.
976    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
977    let mut realm = CurrentRealm::assert(&mut cx);
978    let global_scope = GlobalScope::from_current_realm(&mut realm);
979
980    let cx = &mut realm;
981
982    let args = unsafe { CallArgs::from_vp(vp, argc) };
983
984    rooted!(&in(cx) let module_value = unsafe { *GetFunctionNativeReserved(args.callee(), MODULE_RECORD_SLOT) });
985    assert!(!module_value.is_undefined());
986    rooted!(&in(cx) let module_record = module_value.to_object());
987    rooted!(&in(cx) let mut module_private: JSVal);
988    unsafe { JS_GetModulePrivate(module_record.get(), module_private.handle_mut()) };
989    assert!(!module_private.is_undefined());
990    let module_data = unsafe { module_script_from_reference_private(module_private.handle()) };
991
992    // https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties
993
994    // Step 4.1. Set specifier to ? ToString(specifier).
995    let specifier = unsafe {
996        let value = HandleValue::from_raw(args.get(0));
997
998        match NonNull::new(ToString(cx, value)) {
999            Some(jsstr) => jsstr_to_string(cx, jsstr),
1000            None => return false,
1001        }
1002    };
1003
1004    // Step 4.2. Let url be the result of resolving a module specifier given moduleScript and specifier.
1005    let url = ModuleTree::resolve_module_specifier(&global_scope, module_data, specifier);
1006
1007    match url {
1008        Ok(url) => {
1009            // Step 4.3. Return the serialization of url.
1010            url.as_str()
1011                .safe_to_jsval(cx, unsafe { MutableHandleValue::from_raw(args.rval()) });
1012            true
1013        },
1014        Err(error) => {
1015            let resolution_error = gen_type_error(cx, &global_scope, error);
1016
1017            unsafe {
1018                JS_SetPendingException(
1019                    cx,
1020                    resolution_error.handle(),
1021                    ExceptionStackBehavior::Capture,
1022                );
1023            }
1024            false
1025        },
1026    }
1027}
1028
1029#[expect(clippy::too_many_arguments)]
1030/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree>
1031/// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet/module-worker-script-graph>
1032pub(crate) fn fetch_a_module_script_graph(
1033    cx: &mut JSContext,
1034    global: &GlobalScope,
1035    url: UrlWithBlobClaim,
1036    fetch_client: RequestClient,
1037    destination: Destination,
1038    referrer: Referrer,
1039    credentials_mode: CredentialsMode,
1040    introduction_type: Option<&'static CStr>,
1041    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1042) {
1043    let global_scope = DomRoot::from_ref(global);
1044
1045    // Step 1. Let options be a script fetch options whose cryptographic nonce
1046    // is the empty string, integrity metadata is the empty string, parser
1047    // metadata is "not-parser-inserted", credentials mode is credentialsMode,
1048    // referrer policy is the empty string, and fetch priority is "auto".
1049    let options = ScriptFetchOptions {
1050        integrity_metadata: "".into(),
1051        credentials_mode,
1052        cryptographic_nonce: "".into(),
1053        parser_metadata: ParserMetadata::NotParserInserted,
1054        referrer_policy: ReferrerPolicy::EmptyString,
1055        render_blocking: false,
1056    };
1057
1058    // Step 2. Fetch a single module script given url, fetchClient, destination, options,
1059    // settingsObject, "client", true, and onSingleFetchComplete as defined below.
1060    fetch_a_single_module_script(
1061        cx,
1062        url,
1063        fetch_client.clone(),
1064        global,
1065        destination,
1066        options,
1067        referrer,
1068        None,
1069        true,
1070        introduction_type,
1071        move |cx, module_tree| {
1072            let Some(module) = module_tree else {
1073                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1074                return on_complete(cx, None);
1075            };
1076
1077            // Step 1.2. Fetch the descendants of and link result given fetchClient, destination,
1078            // and onComplete.
1079            fetch_the_descendants_and_link_module_script(
1080                cx,
1081                &global_scope,
1082                module,
1083                fetch_client,
1084                destination,
1085                on_complete,
1086            );
1087        },
1088    );
1089}
1090
1091/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-script-tree>
1092pub(crate) fn fetch_an_external_module_script(
1093    cx: &mut JSContext,
1094    url: UrlWithBlobClaim,
1095    global: &GlobalScope,
1096    options: ScriptFetchOptions,
1097    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1098) {
1099    let referrer = global.get_referrer();
1100    let fetch_client = global.request_client(Some(cx.no_gc()));
1101    let global_scope = DomRoot::from_ref(global);
1102
1103    // Step 1. Fetch a single module script given url, settingsObject, "script", options, settingsObject, "client", true,
1104    // and with the following steps given result:
1105    fetch_a_single_module_script(
1106        cx,
1107        url,
1108        fetch_client.clone(),
1109        global,
1110        Destination::Script,
1111        options,
1112        referrer,
1113        None,
1114        true,
1115        Some(IntroductionType::SRC_SCRIPT),
1116        move |cx, module_tree| {
1117            let Some(module) = module_tree else {
1118                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1119                return on_complete(cx, None);
1120            };
1121
1122            // Step 1.2. Fetch the descendants of and link result given settingsObject, "script", and onComplete.
1123            fetch_the_descendants_and_link_module_script(
1124                cx,
1125                &global_scope,
1126                module,
1127                fetch_client,
1128                Destination::Script,
1129                on_complete,
1130            );
1131        },
1132    );
1133}
1134
1135/// <https://html.spec.whatwg.org/multipage/#fetch-a-modulepreload-module-script-graph>
1136pub(crate) fn fetch_a_modulepreload_module(
1137    cx: &mut JSContext,
1138    url: UrlWithBlobClaim,
1139    destination: Destination,
1140    global: &GlobalScope,
1141    options: ScriptFetchOptions,
1142    on_complete: impl FnOnce(&mut JSContext, bool) + 'static,
1143) {
1144    let referrer = global.get_referrer();
1145    let fetch_client = global.request_client(Some(cx.no_gc()));
1146    let global_scope = DomRoot::from_ref(global);
1147
1148    // Note: There is a specification inconsistency, `fetch_a_single_module_script` doesn't allow
1149    // fetching top level JSON/CSS module scripts, but should be possible when preloading.
1150    let module_type = if let Destination::Json = destination {
1151        Some(ModuleType::JSON)
1152    } else {
1153        None
1154    };
1155
1156    // Step 1. Fetch a single module script given url, settingsObject, destination, options, settingsObject,
1157    // "client", true, and with the following steps given result:
1158    fetch_a_single_module_script(
1159        cx,
1160        url,
1161        fetch_client.clone(),
1162        global,
1163        destination,
1164        options,
1165        referrer,
1166        module_type,
1167        true,
1168        Some(IntroductionType::SRC_SCRIPT),
1169        move |cx, result| {
1170            // Step 1. Run onComplete given result.
1171            on_complete(cx, result.is_none());
1172
1173            // Step 2. Assert: settingsObject's global object implements Window.
1174            assert!(global_scope.is::<Window>());
1175
1176            // Step 3. If result is not null, optionally fetch the descendants of and link result
1177            // given settingsObject, destination, and an empty algorithm.
1178            if pref!(dom_allow_preloading_module_descendants) &&
1179                let Some(module) = result
1180            {
1181                fetch_the_descendants_and_link_module_script(
1182                    cx,
1183                    &global_scope,
1184                    module,
1185                    fetch_client,
1186                    destination,
1187                    |_, _| {},
1188                );
1189            }
1190        },
1191    );
1192}
1193
1194#[expect(clippy::too_many_arguments)]
1195/// <https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph>
1196pub(crate) fn fetch_inline_module_script(
1197    cx: &mut JSContext,
1198    global: &GlobalScope,
1199    module_script_text: Cow<'_, str>,
1200    url: ServoUrl,
1201    options: ScriptFetchOptions,
1202    line_number: u32,
1203    introduction_type: Option<&'static CStr>,
1204    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1205) {
1206    // Step 1. Let script be the result of creating a JavaScript module script using sourceText, settingsObject, baseURL, and options.
1207    let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
1208        cx,
1209        module_script_text,
1210        global,
1211        &url,
1212        options,
1213        false,
1214        line_number,
1215        introduction_type,
1216    ));
1217    let fetch_client = global.request_client(Some(cx.no_gc()));
1218
1219    // Step 2. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
1220    fetch_the_descendants_and_link_module_script(
1221        cx,
1222        global,
1223        module_tree,
1224        fetch_client,
1225        Destination::Script,
1226        on_complete,
1227    );
1228}
1229
1230/// <https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script>
1231fn fetch_the_descendants_and_link_module_script(
1232    cx: &mut JSContext,
1233    global: &GlobalScope,
1234    module_script: Rc<ModuleTree>,
1235    fetch_client: RequestClient,
1236    destination: Destination,
1237    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1238) {
1239    // Step 1. Let record be moduleScript's record.
1240    // Step 2. If record is null, then:
1241    let Some(record) = module_script.get_record() else {
1242        let parse_error = module_script.get_parse_error().cloned();
1243
1244        // Step 2.1. Set moduleScript's error to rethrow to moduleScript's parse error.
1245        module_script.set_rethrow_error(parse_error.unwrap());
1246
1247        // Step 2.2. Run onComplete given moduleScript.
1248        on_complete(cx, Some(module_script));
1249
1250        // Step 2.3. Return.
1251        return;
1252    };
1253
1254    // Step 3. Let state be Record
1255    // { [[ErrorToRethrow]]: null, [[Destination]]: destination, [[PerformFetch]]: null, [[FetchClient]]: fetchClient }.
1256    let state = Box::new(LoadState {
1257        error_to_rethrow: RefCell::new(None),
1258        destination,
1259        fetch_client,
1260        module_script: DomRefCell::new(Some(module_script.clone())),
1261        on_complete: DomRefCell::new(Some(Box::new(on_complete))),
1262    });
1263
1264    // TODO Step 4. If performFetch was given, set state.[[PerformFetch]] to performFetch.
1265
1266    let mut realm = enter_auto_realm(cx, global);
1267    let cx = &mut realm.current_realm();
1268
1269    // Step 5. Let loadingPromise be record.LoadRequestedModules(state).
1270    load_requested_modules(cx, record.handle(), state);
1271}
1272
1273/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
1274#[expect(clippy::too_many_arguments)]
1275pub(crate) fn fetch_a_single_module_script(
1276    cx: &mut JSContext,
1277    url: UrlWithBlobClaim,
1278    fetch_client: RequestClient,
1279    global: &GlobalScope,
1280    destination: Destination,
1281    options: ScriptFetchOptions,
1282    referrer: Referrer,
1283    module_type: Option<ModuleType>,
1284    is_top_level: bool,
1285    introduction_type: Option<&'static CStr>,
1286    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
1287) {
1288    // Step 1. Let moduleType be "javascript-or-wasm".
1289    // Step 2. If moduleRequest was given, then set moduleType to the result of running the
1290    // module type from module request steps given moduleRequest.
1291    let module_type = module_type.unwrap_or(ModuleType::JavaScript);
1292
1293    // TODO Step 3. Assert: the result of running the module type allowed steps given moduleType and settingsObject is true.
1294    // Otherwise, we would not have reached this point because a failure would have been raised
1295    // when inspecting moduleRequest.[[Attributes]] in HostLoadImportedModule or fetch a single imported module script.
1296
1297    let module_request = (url.url(), module_type);
1298
1299    // Step 4. Let moduleMap be settingsObject's module map.
1300    let module_map = global.module_map();
1301    let mut module_map_borrow = module_map.safe_borrow_mut(cx);
1302
1303    let entry = module_map_borrow.entry(module_request.clone());
1304
1305    match entry {
1306        Entry::Occupied(mut entry) => match entry.get_mut() {
1307            // Step 5. If moduleMap[(url, moduleType)] is a module script, run onComplete given
1308            // moduleMap[(url, moduleType)], and return.
1309            ModuleStatus::Loaded(module_tree) => {
1310                let module = module_tree.clone();
1311                drop(module_map_borrow);
1312                return on_complete(cx, Some(module));
1313            },
1314            // Step 6. If moduleMap[(url, moduleType)] is a list, append onComplete to
1315            // moduleMap[(url, moduleType)], and return.
1316            ModuleStatus::Fetching(callbacks) => return callbacks.push(Box::new(on_complete)),
1317        },
1318        // Step 7. Set moduleMap[(url, moduleType)] to « onComplete ».
1319        Entry::Vacant(entry) => {
1320            entry.insert(ModuleStatus::Fetching(vec![Box::new(on_complete)]));
1321        },
1322    }
1323
1324    // We only need a policy container when fetching the root of a module worker.
1325    let policy_container = (is_top_level && global.is::<WorkerGlobalScope>())
1326        .then(|| fetch_client.policy_container.clone());
1327
1328    // Step 8. Let request be a new request whose URL is url, mode is "cors", referrer is referrer, and client is fetchClient.
1329
1330    // Step 10. If destination is "worker", "sharedworker", or "serviceworker", and isTopLevel is true,
1331    // then set request's mode to "same-origin".
1332    let mode = match destination {
1333        Destination::Worker | Destination::SharedWorker if is_top_level => RequestMode::SameOrigin,
1334        _ => RequestMode::CorsMode,
1335    };
1336
1337    // Step 9. Set request's destination to the result of running the
1338    // fetch destination from module type steps given destination and moduleType.
1339    let destination = match module_type {
1340        ModuleType::JSON => Destination::Json,
1341        ModuleType::CSS => todo!("https://github.com/servo/servo/issues/47179"),
1342        ModuleType::Text => {
1343            todo!("https://github.com/servo/servo/issues/47149")
1344        },
1345        ModuleType::Bytes => unreachable!("Not in ESR153"),
1346        ModuleType::JavaScript | ModuleType::Unknown => destination,
1347    };
1348
1349    // TODO Step 11. Set request's initiator type to "script".
1350
1351    // Step 12. Set up the module script request given request and options.
1352    let request = RequestBuilder::new(global.webview_id(), url, referrer)
1353        .destination(destination)
1354        .parser_metadata(options.parser_metadata)
1355        .integrity_metadata(options.integrity_metadata.clone())
1356        .credentials_mode(options.credentials_mode)
1357        .referrer_policy(options.referrer_policy)
1358        .mode(mode)
1359        .cryptographic_nonce_metadata(options.cryptographic_nonce.clone())
1360        .client(fetch_client)
1361        .pipeline_id(Some(global.pipeline_id()));
1362
1363    let context = ModuleContext {
1364        owner: Trusted::new(global),
1365        data: BytesMut::new(),
1366        metadata: None,
1367        module_request,
1368        options,
1369        status: Ok(()),
1370        introduction_type,
1371        policy_container,
1372    };
1373
1374    let task_source = global.task_manager().networking_task_source().to_sendable();
1375    global.fetch(request, context, task_source);
1376}
1377
1378/// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record>
1379#[derive(Default, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
1380pub(crate) struct ResolvedModule {
1381    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-serialized-base-url>
1382    pub(crate) base_url: String,
1383    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-specifier>
1384    pub(crate) specifier: String,
1385    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-as-url>
1386    #[no_trace]
1387    pub(crate) specifier_url: Option<ServoUrl>,
1388}
1389
1390impl ResolvedModule {
1391    pub(crate) fn new(
1392        base_url: String,
1393        specifier: String,
1394        specifier_url: Option<ServoUrl>,
1395    ) -> Self {
1396        Self {
1397            base_url,
1398            specifier,
1399            specifier_url,
1400        }
1401    }
1402}
1403
1404/// <https://html.spec.whatwg.org/multipage/#resolving-an-imports-match>
1405///
1406/// When the error is thrown, it will terminate the entire resolve a module specifier algorithm
1407/// without any further fallbacks.
1408fn resolve_imports_match(
1409    normalized_specifier: &str,
1410    as_url: Option<&ServoUrl>,
1411    specifier_map: &ModuleSpecifierMap,
1412) -> Fallible<Option<ServoUrl>> {
1413    // Step 1. For each specifierKey → resolutionResult of specifierMap:
1414    for (specifier_key, resolution_result) in specifier_map {
1415        // Step 1.1 If specifierKey is normalizedSpecifier, then:
1416        if specifier_key == normalized_specifier {
1417            if let Some(resolution_result) = resolution_result {
1418                // Step 1.1.2 Assert: resolutionResult is a URL.
1419                // This is checked by Url type already.
1420                // Step 1.1.3 Return resolutionResult.
1421                return Ok(Some(resolution_result.clone()));
1422            } else {
1423                // Step 1.1.1 If resolutionResult is null, then throw a TypeError.
1424                return Err(Error::Type(
1425                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
1426                ));
1427            }
1428        }
1429
1430        // Step 1.2 If all of the following are true:
1431        // - specifierKey ends with U+002F (/)
1432        // - specifierKey is a code unit prefix of normalizedSpecifier
1433        // - either asURL is null, or asURL is special, then:
1434        if specifier_key.ends_with('\u{002f}') &&
1435            normalized_specifier.starts_with(specifier_key) &&
1436            (as_url.is_none() || as_url.is_some_and(|u| u.is_special_scheme()))
1437        {
1438            // Step 1.2.1 If resolutionResult is null, then throw a TypeError.
1439            // Step 1.2.2 Assert: resolutionResult is a URL.
1440            let Some(resolution_result) = resolution_result else {
1441                return Err(Error::Type(
1442                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
1443                ));
1444            };
1445
1446            // Step 1.2.3 Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.
1447            let after_prefix = normalized_specifier
1448                .strip_prefix(specifier_key)
1449                .expect("specifier_key should be the prefix of normalized_specifier");
1450
1451            // Step 1.2.4 Assert: resolutionResult, serialized, ends with U+002F (/), as enforced during parsing.
1452            debug_assert!(resolution_result.as_str().ends_with('\u{002f}'));
1453
1454            // Step 1.2.5 Let url be the result of URL parsing afterPrefix with resolutionResult.
1455            let url = ServoUrl::parse_with_base(Some(resolution_result), after_prefix);
1456
1457            // Step 1.2.6 If url is failure, then throw a TypeError
1458            // Step 1.2.7 Assert: url is a URL.
1459            let Ok(url) = url else {
1460                return Err(Error::Type(
1461                    c"Resolution of normalizedSpecifier was blocked since
1462                    the afterPrefix portion could not be URL-parsed relative to
1463                    the resolutionResult mapped to by the specifierKey prefix."
1464                        .to_owned(),
1465                ));
1466            };
1467
1468            // Step 1.2.8 If the serialization of resolutionResult is not
1469            // a code unit prefix of the serialization of url, then throw a TypeError
1470            if !url.as_str().starts_with(resolution_result.as_str()) {
1471                return Err(Error::Type(
1472                    c"Resolution of normalizedSpecifier was blocked due to
1473                    it backtracking above its prefix specifierKey."
1474                        .to_owned(),
1475                ));
1476            }
1477
1478            // Step 1.2.9 Return url.
1479            return Ok(Some(url));
1480        }
1481    }
1482
1483    // Step 2. Return null.
1484    Ok(None)
1485}