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