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