Skip to main content

script/scripting/
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;
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, GetFunctionNativeReserved, GetModuleLoadHook,
26    Handle as RawHandle, HandleValue as RawHandleValue, Heap, JS_GetFunctionObject,
27    JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE, JSRuntime, JSScript, ModuleType,
28    SetFunctionNativeReserved, SetModuleLoadHook, SetModuleMetadataHook, SetModulePrivate,
29    SetScriptPrivateReferenceHooks, Value,
30};
31use js::jsval::{JSVal, ObjectValue, PrivateValue, UndefinedValue};
32use js::realm::CurrentRealm;
33use js::rust::wrappers2::{
34    CompileJsonModule1, CompileModule1, CreateDefaultExportSyntheticModule,
35    DefineFunctionWithReserved, GetModuleRequestSpecifier, JS_ClearPendingException,
36    JS_DefineProperty4, JS_GetModulePrivate, JS_GetPendingException, JS_NewStringCopyN,
37};
38use js::rust::{Handle, HandleValue, ToString, transform_str_to_source_text};
39use mime::Mime;
40use net_traits::blob_url_store::UrlWithBlobClaim;
41use net_traits::http_status::HttpStatus;
42use net_traits::mime_classifier::MimeClassifier;
43use net_traits::policy_container::PolicyContainer;
44use net_traits::request::{
45    CredentialsMode, Destination, ParserMetadata, Referrer, RequestBuilder, RequestClient,
46    RequestId, RequestMode,
47};
48use net_traits::{FetchMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming};
49use script_bindings::cell::DomRefCell;
50use script_bindings::reflector::DomObject;
51use script_bindings::trace::CustomTraceable;
52use servo_config::pref;
53use servo_url::ServoUrl;
54
55use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::{
56    CSSStyleSheetInit, CSSStyleSheetMethods,
57};
58use crate::dom::bindings::codegen::UnionTypes::MediaListOrString;
59use crate::dom::bindings::error::throw_dom_exception;
60use crate::dom::bindings::inheritance::Castable;
61use crate::dom::bindings::refcounted::Trusted;
62use crate::dom::bindings::root::DomRoot;
63use crate::dom::bindings::str::{DOMString, USVString};
64use crate::dom::bindings::trace::RootedTraceableBox;
65use crate::dom::csp::{GlobalCspReporting, Violation};
66use crate::dom::css::cssstylesheet::CSSStyleSheet;
67use crate::dom::globalscope::GlobalScope;
68use crate::dom::html::htmlscriptelement::substitute_with_local_script;
69use crate::dom::performance::performanceresourcetiming::InitiatorType;
70use crate::dom::promisenativehandler::Callback;
71use crate::dom::types::{
72    DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, WorkerGlobalScope, WorkletGlobalScope,
73};
74use crate::dom::window::Window;
75use crate::fetch::network_listener::{self, FetchResponseListener, ResourceTimingListener};
76use crate::realms::enter_auto_realm;
77use crate::runtime::script_runtime::IntroductionType;
78use crate::scripting::fetching_options::{BaseScript, ScriptFetchOptions};
79use crate::scripting::import_map::ImportMap;
80use crate::scripting::module_loading::{
81    LoadState, host_load_imported_module, load_requested_modules,
82};
83use crate::scripting::script_execution::{ScriptOptions, fill_compile_options};
84use crate::tasks::task::NonSendTaskBox;
85use crate::unminify::{ScriptSource, unminify_js};
86
87#[derive(JSTraceable)]
88pub(crate) struct ModuleObject(RootedTraceableBox<Heap<*mut JSObject>>);
89
90impl ModuleObject {
91    pub(crate) fn new(obj: HandleObject) -> ModuleObject {
92        ModuleObject(RootedTraceableBox::from_box(Heap::boxed(obj.get())))
93    }
94
95    pub(crate) fn handle(&'_ self) -> HandleObject<'_> {
96        self.0.handle()
97    }
98}
99
100#[derive(JSTraceable)]
101pub(crate) struct RethrowError(RootedTraceableBox<Heap<JSVal>>);
102
103impl RethrowError {
104    pub(crate) fn new(val: Box<Heap<JSVal>>) -> Self {
105        Self(RootedTraceableBox::from_box(val))
106    }
107
108    #[expect(unsafe_code)]
109    pub(crate) fn from_pending_exception(cx: &mut JSContext) -> Self {
110        rooted!(&in(cx) let mut exception = UndefinedValue());
111        assert!(unsafe { JS_GetPendingException(cx, exception.handle_mut()) });
112        unsafe { JS_ClearPendingException(cx) };
113
114        Self::new(Heap::boxed(exception.get()))
115    }
116
117    pub(crate) fn handle(&self) -> Handle<'_, JSVal> {
118        self.0.handle()
119    }
120}
121
122impl Debug for RethrowError {
123    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
124        "RethrowError(...)".fmt(fmt)
125    }
126}
127
128impl Clone for RethrowError {
129    fn clone(&self) -> Self {
130        Self(RootedTraceableBox::from_box(Heap::boxed(self.0.get())))
131    }
132}
133
134pub(crate) type ModuleRequest = (ServoUrl, ModuleType);
135
136#[derive(JSTraceable)]
137pub(crate) enum ModuleStatus {
138    #[expect(clippy::type_complexity)]
139    Fetching(#[no_trace] Vec<Box<dyn FnOnce(&mut JSContext, Option<Rc<ModuleTree>>)>>),
140    Loaded(Rc<ModuleTree>),
141}
142
143#[derive(Default, JSTraceable, MallocSizeOf)]
144pub(crate) struct ModuleTree {
145    #[ignore_malloc_size_of = "mozjs"]
146    record: OnceCell<ModuleObject>,
147    #[ignore_malloc_size_of = "mozjs"]
148    parse_error: OnceCell<RethrowError>,
149    #[ignore_malloc_size_of = "mozjs"]
150    rethrow_error: DomRefCell<Option<RethrowError>>,
151}
152
153impl ModuleTree {
154    pub(crate) fn get_record(&self) -> Option<&ModuleObject> {
155        self.record.get()
156    }
157
158    pub(crate) fn get_parse_error(&self) -> Option<&RethrowError> {
159        self.parse_error.get()
160    }
161
162    pub(crate) fn get_rethrow_error(&self) -> &DomRefCell<Option<RethrowError>> {
163        &self.rethrow_error
164    }
165
166    pub(crate) fn set_rethrow_error(&self, rethrow_error: RethrowError) {
167        *self.rethrow_error.borrow_mut() = Some(rethrow_error);
168    }
169}
170
171impl ModuleTree {
172    #[expect(unsafe_code)]
173    #[expect(clippy::too_many_arguments)]
174    /// <https://html.spec.whatwg.org/multipage/#creating-a-javascript-module-script>
175    fn create_a_javascript_module_script(
176        cx: &mut CurrentRealm,
177        source: Cow<'_, str>,
178        global: &GlobalScope,
179        url: &ServoUrl,
180        options: ScriptFetchOptions,
181        external: bool,
182        line_number: u32,
183        introduction_type: Option<&'static CStr>,
184    ) -> Self {
185        let owner = Trusted::new(global);
186
187        // Step 2. Let script be a new module script that this algorithm will subsequently initialize.
188        // Step 6. Set script's parse error and error to rethrow to null.
189        let module = ModuleTree::default();
190
191        let compile_options = fill_compile_options(
192            cx,
193            url.as_str(),
194            ScriptOptions::empty(),
195            introduction_type,
196            line_number,
197        );
198
199        let mut module_source = ScriptSource {
200            source,
201            external,
202            url,
203        };
204        if let Some(unminified_js_dir) = global.unminified_js_dir() {
205            unminify_js(&mut module_source, unminified_js_dir);
206        }
207        let mut source = transform_str_to_source_text(&module_source.source);
208
209        unsafe {
210            // Step 7. Let result be ParseModule(source, settings's realm, script).
211            rooted!(&in(cx) let module_script = CompileModule1(cx, compile_options.ptr, &mut source));
212
213            // Step 8. If result is a list of errors, then:
214            if module_script.is_null() {
215                warn!("fail to compile module script of {}", url);
216
217                // Step 8.1. Set script's parse error to result[0].
218                let _ = module
219                    .parse_error
220                    .set(RethrowError::from_pending_exception(cx));
221
222                // Step 8.2. Return script.
223                return module;
224            }
225
226            // Step 3. Set script's settings object to settings.
227            // Step 4. Set script's base URL to baseURL.
228            // Step 5. Set script's fetch options to options.
229            let module_script_data = Rc::new(BaseScript::new(url.clone(), options, Some(owner)));
230
231            SetModulePrivate(
232                module_script.get(),
233                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
234            );
235
236            // Step 9. Set script's record to result.
237            let _ = module.record.set(ModuleObject::new(module_script.handle()));
238        }
239
240        // Step 10. Return script.
241        module
242    }
243
244    #[expect(unsafe_code)]
245    /// <https://html.spec.whatwg.org/multipage/#creating-a-json-module-script>
246    fn create_a_json_module_script(
247        cx: &mut CurrentRealm,
248        source: &str,
249        url: &ServoUrl,
250        introduction_type: Option<&'static CStr>,
251    ) -> Self {
252        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
253        // Step 4. Set script's parse error and error to rethrow to null.
254        let module = ModuleTree::default();
255
256        // Step 2. Set script's settings object to settings.
257        // Step 3. Set script's base URL and fetch options to null.
258        // Note: We don't need to call `SetModulePrivate` for json scripts
259
260        let compile_options = fill_compile_options(
261            cx,
262            url.as_str(),
263            ScriptOptions::empty(),
264            introduction_type,
265            1, // line_number
266        );
267
268        let mut source = transform_str_to_source_text(source);
269
270        // Step 5. Let result be ParseJSONModule(source).
271        rooted!(&in(cx) let module_script = unsafe { CompileJsonModule1(cx, compile_options.ptr, &mut source) });
272
273        // If this throws an exception, catch it, and set script's parse error to that exception, and return script.
274        if module_script.is_null() {
275            warn!("fail to compile module script of {}", url);
276
277            let _ = module
278                .parse_error
279                .set(RethrowError::from_pending_exception(cx));
280            return module;
281        }
282
283        // Step 6. Set script's record to result.
284        let _ = module.record.set(ModuleObject::new(module_script.handle()));
285
286        // Step 7. Return script.
287        module
288    }
289
290    #[expect(unsafe_code)]
291    /// <https://html.spec.whatwg.org/multipage/#creating-a-css-module-script>
292    fn create_a_css_module_script(
293        cx: &mut CurrentRealm,
294        source: &str,
295        global: &GlobalScope,
296        url: ServoUrl,
297    ) -> Self {
298        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
299        // Step 4. Set script's parse error and error to rethrow to null.
300        let script = ModuleTree::default();
301
302        // Step 2. Set script's settings object to settings.
303        // Step 3. Set script's base URL and fetch options to null.
304        // Note: We don't need to call `SetModulePrivate` for css modules.
305
306        // Step 5. Let sheet be the result of running the steps to create a constructed
307        // CSSStyleSheet with an empty dictionary as the argument.
308        let dictionary = CSSStyleSheetInit {
309            baseURL: Some(url.into_string().into()),
310            disabled: false,
311            media: MediaListOrString::String(DOMString::new()),
312        };
313        let sheet = CSSStyleSheet::Constructor(cx, global.as_window(), None, &dictionary);
314
315        // Step 6. Run the steps to synchronously replace the rules of a CSSStyleSheet on sheet
316        // given source.
317        if let Err(error) = sheet.ReplaceSync(cx, USVString::from(source.to_owned())) {
318            // If this throws an exception, catch it, and set script's parse error to that exception,
319            // and return script.
320            throw_dom_exception(cx, global, error);
321
322            let _ = script
323                .parse_error
324                .set(RethrowError::from_pending_exception(cx));
325            return script;
326        }
327
328        // Step 7. Set script's record to the result of CreateDefaultExportSyntheticModule(sheet).
329        rooted!(&in(cx) let sheet = ObjectValue(sheet.reflector().get_jsobject().get()));
330        rooted!(&in(cx) let module_script = unsafe { CreateDefaultExportSyntheticModule(cx, sheet.handle()) });
331        let _ = script.record.set(ModuleObject::new(module_script.handle()));
332
333        // Step 8. Return script.
334        script
335    }
336
337    #[expect(unsafe_code)]
338    /// <https://html.spec.whatwg.org/multipage/#creating-a-text-module-script>
339    fn create_a_text_module_script(cx: &mut CurrentRealm, source: &str) -> Self {
340        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
341        // Step 4. Set script's parse error and error to rethrow to null.
342        let script = ModuleTree::default();
343
344        // Step 2. Set script's settings object to settings.
345        // Step 3. Set script's base URL and fetch options to null.
346        // Note: We don't need to call `SetModulePrivate` for text modules.
347
348        // Step 5. Let result be CreateTextModule(text).
349        rooted!(&in(cx) let mut text = UndefinedValue());
350        source.to_jsval(cx, text.handle_mut());
351        rooted!(&in(cx) let result = unsafe { CreateDefaultExportSyntheticModule(cx, text.handle()) });
352
353        // Step 6. Set script's record to result.
354        let _ = script.record.set(ModuleObject::new(result.handle()));
355
356        // Step 7. Return script.
357        script
358    }
359}
360
361#[derive(JSTraceable, MallocSizeOf)]
362pub(crate) struct ModuleHandler {
363    #[ignore_malloc_size_of = "Measuring trait objects is hard"]
364    task: DomRefCell<Option<Box<dyn NonSendTaskBox>>>,
365}
366
367impl ModuleHandler {
368    pub(crate) fn new_boxed(task: Box<dyn NonSendTaskBox>) -> Box<dyn Callback> {
369        Box::new(Self {
370            task: DomRefCell::new(Some(task)),
371        })
372    }
373}
374
375impl Callback for ModuleHandler {
376    fn callback(&self, cx: &mut CurrentRealm, _v: HandleValue) {
377        let task = self.task.borrow_mut().take().unwrap();
378        task.run_box(cx);
379    }
380}
381
382/// The context required for asynchronously loading an external module script source.
383struct ModuleContext {
384    /// The owner of the module that initiated the request.
385    owner: Trusted<GlobalScope>,
386    /// The response body received to date.
387    data: BytesMut,
388    /// The response metadata received to date.
389    metadata: Option<Metadata>,
390    /// Url and type of the requested module.
391    module_request: ModuleRequest,
392    /// Options for the current script fetch
393    options: ScriptFetchOptions,
394    /// Indicates whether the request failed, and why
395    status: Result<(), NetworkError>,
396    /// `introductionType` value to set in the `CompileOptionsWrapper`.
397    introduction_type: Option<&'static CStr>,
398    /// <https://html.spec.whatwg.org/multipage/#policy-container>
399    policy_container: Option<Arc<PolicyContainer>>,
400}
401
402impl FetchResponseListener for ModuleContext {
403    // TODO(cybai): Perhaps add custom steps to perform fetch here?
404    fn process_request_body(&mut self, _: RequestId) {}
405
406    fn process_response(
407        &mut self,
408        _: &mut JSContext,
409        _: RequestId,
410        metadata: Result<FetchMetadata, NetworkError>,
411    ) {
412        self.metadata = metadata.ok().map(Into::into);
413
414        let status = self
415            .metadata
416            .as_ref()
417            .map(|m| m.status.clone())
418            .unwrap_or_else(HttpStatus::new_error);
419
420        self.status = {
421            if status.is_error() {
422                Err(NetworkError::ResourceLoadError(
423                    "No http status code received".to_owned(),
424                ))
425            } else if status.is_success() {
426                Ok(())
427            } else {
428                Err(NetworkError::ResourceLoadError(format!(
429                    "HTTP error code {}",
430                    status.code()
431                )))
432            }
433        };
434    }
435
436    fn process_response_chunk(&mut self, _: &mut JSContext, _: RequestId, chunk: Bytes) {
437        if self.status.is_ok() {
438            self.data.extend_from_slice(&chunk);
439        }
440    }
441
442    /// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
443    /// Step 13
444    fn process_response_eof(
445        mut self,
446        cx: &mut JSContext,
447        _: RequestId,
448        response: Result<(), NetworkError>,
449        timing: ResourceFetchTiming,
450    ) {
451        let global = self.owner.root();
452        let (_url, module_type) = &self.module_request;
453
454        if !global.is::<WorkletGlobalScope>() {
455            network_listener::submit_timing(cx, &self, &response, &timing);
456        }
457
458        // Step 1. If any of the following are true: bodyBytes is null or failure; or response's
459        // status is not an ok status, then:
460        if let (Err(error), _) | (_, Err(error)) = (response.as_ref(), self.status.as_ref()) {
461            error!("Fetching module script failed {:?}", error);
462            // Step 1.1. Let callbacks be moduleMap[(url, moduleType)].
463            // Step 1.2. Remove moduleMap[(url, moduleType)].
464            let Some(ModuleStatus::Fetching(callbacks)) = global.with_module_map(|module_map| {
465                module_map.safe_borrow_mut(cx).remove(&self.module_request)
466            }) else {
467                return error!("Processing response for a non pending module request");
468            };
469
470            // Step 1.3. For each callback of callbacks: run callback given null.
471            for callback in callbacks {
472                (callback)(cx, None);
473            }
474
475            // Step 1.4. Return.
476            return;
477        }
478
479        let metadata = self.metadata.take().unwrap();
480
481        // The processResponseConsumeBody steps defined inside
482        // [run a worker](https://html.spec.whatwg.org/multipage/#run-a-worker)
483        if let Some(policy_container) = self.policy_container {
484            let workerscope = global.downcast::<WorkerGlobalScope>().expect(
485                "We only need a policy container when initializing a worker's globalscope.",
486            );
487            workerscope.process_response_for_workerscope(&metadata, &policy_container);
488        }
489
490        let final_url = metadata.final_url;
491
492        // Step 2. Let mimeType be the result of extracting a MIME type from response's header list.
493        let mime_type: Option<Mime> = metadata.content_type.map(Serde::into_inner).map(Into::into);
494
495        // Step 3. Let moduleScript be null.
496        let mut module_script = None;
497
498        // Step 4. Let referrerPolicy be the result of parsing the `Referrer-Policy` header given response. [REFERRERPOLICY]
499        let referrer_policy = metadata
500            .headers
501            .and_then(|headers| headers.typed_get::<ReferrerPolicyHeader>())
502            .into();
503
504        // Step 5. If referrerPolicy is not the empty string, set options's referrer policy to referrerPolicy.
505        if referrer_policy != ReferrerPolicy::EmptyString {
506            self.options.referrer_policy = referrer_policy;
507        }
508
509        let mut realm = enter_auto_realm(cx, &*global);
510        let cx = &mut realm.current_realm();
511
512        // TODO Step 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set
513        // moduleScript to the result of creating a WebAssembly module script given bodyBytes, settingsObject, response's URL, and options.
514
515        // Step 7.1 Let sourceText be the result of UTF-8 decoding bodyBytes.
516        let (mut source_text, _) = UTF_8.decode_with_bom_removal(&self.data);
517
518        match (module_type, mime_type) {
519            // Step 7.2. If moduleType is "text", then set moduleScript to the result of creating a
520            // text module script given sourceText and settingsObject.
521            (ModuleType::Text, _) => {
522                let module_tree =
523                    Rc::new(ModuleTree::create_a_text_module_script(cx, &source_text));
524                module_script = Some(module_tree);
525            },
526            // Step 7.3. If mimeType is a JavaScript MIME type and moduleType is
527            // "javascript-or-wasm", then set moduleScript to the result of creating a JavaScript
528            // module script given sourceText, settingsObject, response's URL, and options.
529            (ModuleType::JavaScript, Some(mime)) if MimeClassifier::is_javascript(&mime) => {
530                if let Some(window) = global.downcast::<Window>() &&
531                    let Some(script_souce) = window.local_script_source()
532                {
533                    substitute_with_local_script(script_souce, &mut source_text, &final_url);
534                }
535
536                let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
537                    cx,
538                    source_text,
539                    &global,
540                    &final_url,
541                    self.options,
542                    true,
543                    1,
544                    self.introduction_type,
545                ));
546                module_script = Some(module_tree);
547            },
548            // Step 7.4. If the MIME type essence of mimeType is "text/css" and moduleType is "css",
549            // then set moduleScript to the result of creating a CSS module script given sourceText
550            // and settingsObject.
551            (ModuleType::CSS, Some(mime)) if MimeClassifier::is_css(&mime) => {
552                let module_tree = Rc::new(ModuleTree::create_a_css_module_script(
553                    cx,
554                    &source_text,
555                    &global,
556                    final_url.clone(),
557                ));
558                module_script = Some(module_tree);
559            },
560            // Step 7.5. If mimeType is a JSON MIME type and moduleType is "json", then set
561            // moduleScript to the result of creating a JSON module script given sourceText and settingsObject.
562            (ModuleType::JSON, Some(mime)) if MimeClassifier::is_json(&mime) => {
563                let module_tree = Rc::new(ModuleTree::create_a_json_module_script(
564                    cx,
565                    &source_text,
566                    &final_url,
567                    self.introduction_type,
568                ));
569                module_script = Some(module_tree);
570            },
571            _ => {},
572        }
573
574        let callbacks = global.with_module_map(|module_map| {
575            match module_map
576                .safe_borrow_mut(cx)
577                .entry(self.module_request.clone())
578            {
579                Entry::Occupied(mut entry) => {
580                    // Step 9. If moduleScript is null, then remove moduleMap[(url, moduleType)];
581                    // otherwise set moduleMap[(url, moduleType)] to moduleScript.
582                    let old_value = match module_script.as_ref() {
583                        None => entry.remove(),
584                        Some(module_script) => {
585                            entry.insert(ModuleStatus::Loaded(module_script.clone()))
586                        },
587                    };
588
589                    match old_value {
590                        ModuleStatus::Loaded(_) => None,
591                        ModuleStatus::Fetching(callbacks) => Some(callbacks),
592                    }
593                },
594                Entry::Vacant(_) => None,
595            }
596        });
597
598        if let Some(callbacks) = callbacks {
599            // Step 10. For each callback of callbacks: run callback given moduleScript.
600            for callback in callbacks {
601                (callback)(cx, module_script.clone());
602            }
603        } else {
604            error!("Processing response for a non pending module request");
605        }
606    }
607
608    fn process_csp_violations(
609        &mut self,
610        cx: &mut JSContext,
611        _request_id: RequestId,
612        violations: Vec<Violation>,
613    ) {
614        let global = self.owner.root();
615        if let Some(scope) = global.downcast::<DedicatedWorkerGlobalScope>() {
616            scope.report_csp_violations(violations);
617        } else if let Some(scope) = global.downcast::<SharedWorkerGlobalScope>() {
618            scope.report_csp_violations(violations);
619        } else {
620            global.report_csp_violations(cx, violations, None, None);
621        }
622    }
623
624    fn process_content_length(&mut self, _request_id: RequestId, size: usize) {
625        self.data.reserve(size.saturating_sub(self.data.len()));
626    }
627}
628
629impl ResourceTimingListener for ModuleContext {
630    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
631        let initiator_type = InitiatorType::LocalName("module".to_string());
632        let (url, _) = &self.module_request;
633        (initiator_type, url.clone())
634    }
635
636    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
637        self.owner.root()
638    }
639}
640
641#[expect(unsafe_code)]
642#[expect(non_snake_case)]
643/// A function to register module hooks (e.g. listening on resolving modules,
644/// getting module metadata, getting script private reference and resolving dynamic import)
645pub(crate) unsafe fn EnsureModuleHooksInitialized(rt: *mut JSRuntime) {
646    unsafe {
647        if GetModuleLoadHook(rt).is_some() {
648            return;
649        }
650
651        SetModuleLoadHook(rt, Some(HostLoadImportedModule));
652        SetModuleMetadataHook(rt, Some(HostPopulateImportMeta));
653        SetScriptPrivateReferenceHooks(
654            rt,
655            Some(host_add_ref_top_level_script),
656            Some(host_release_top_level_script),
657        );
658    }
659}
660
661#[expect(unsafe_code)]
662unsafe extern "C" fn host_add_ref_top_level_script(value: *const Value) {
663    let val = unsafe { Rc::from_raw((*value).to_private() as *const BaseScript) };
664    std::mem::forget(val.clone());
665    std::mem::forget(val);
666}
667
668#[expect(unsafe_code)]
669unsafe extern "C" fn host_release_top_level_script(value: *const Value) {
670    let _val = unsafe { Rc::from_raw((*value).to_private() as *const BaseScript) };
671}
672
673#[expect(unsafe_code)]
674pub(crate) unsafe fn module_script_from_reference_private<'a>(
675    reference_private: Handle<'a, JSVal>,
676) -> Option<&'a BaseScript> {
677    if reference_private.get().is_undefined() {
678        return None;
679    }
680    unsafe { (reference_private.get().to_private() as *const BaseScript).as_ref() }
681}
682
683#[expect(unsafe_code)]
684#[expect(non_snake_case)]
685/// <https://tc39.es/ecma262/#sec-HostLoadImportedModule>
686/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
687unsafe extern "C" fn HostLoadImportedModule(
688    cx: *mut RawJSContext,
689    referrer: RawHandle<*mut JSScript>,
690    module_request: RawHandle<*mut JSObject>,
691    host_defined: RawHandleValue,
692    payload: RawHandleValue,
693    _line_number: u32,
694    _column_number: ColumnNumberOneOrigin,
695) -> bool {
696    // SAFETY: it is safe to construct a JSContext from engine hook.
697    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
698    let mut realm = CurrentRealm::assert(&mut cx);
699    let cx = &mut realm;
700
701    let referrer = unsafe { Handle::from_raw(referrer) };
702    let module_request = unsafe { Handle::from_raw(module_request) };
703    let host_defined = unsafe { Handle::from_raw(host_defined) };
704    let payload = unsafe { Handle::from_raw(payload) };
705
706    let jsstr = unsafe { GetModuleRequestSpecifier(cx, module_request) };
707    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
708
709    host_load_imported_module(
710        cx,
711        referrer,
712        module_request,
713        specifier,
714        host_defined,
715        payload,
716    );
717    true
718}
719
720/// <https://searchfox.org/firefox-main/rev/9a8a80db6ce10ffc2fc91a1e25685eed59ce3501/js/loader/ModuleLoaderBase.h#597>
721const MODULE_RECORD_SLOT: usize = 0;
722
723#[expect(unsafe_code)]
724#[expect(non_snake_case)]
725/// <https://tc39.es/ecma262/#sec-hostgetimportmetaproperties>
726/// <https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties>
727unsafe extern "C" fn HostPopulateImportMeta(
728    cx: *mut RawJSContext,
729    module_record: RawHandle<*mut JSObject>,
730    meta_object: RawHandle<*mut JSObject>,
731) -> bool {
732    // SAFETY: it is safe to construct a JSContext from engine hook.
733    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
734    let mut realm = CurrentRealm::assert(&mut cx);
735    let global_scope = GlobalScope::from_current_realm(&mut realm);
736
737    // Step 2.
738    rooted!(&in(cx) let mut module_private: JSVal);
739    unsafe { JS_GetModulePrivate(module_record.get(), module_private.handle_mut()) };
740    let base_url = match unsafe { module_script_from_reference_private(module_private.handle()) } {
741        Some(module_data) => module_data.base_url.clone(),
742        None => global_scope.api_base_url(),
743    };
744
745    unsafe {
746        let url_string = JS_NewStringCopyN(
747            &mut cx,
748            base_url.as_str().as_ptr() as *const _,
749            base_url.as_str().len(),
750        );
751        rooted!(&in(cx) let url_string = url_string);
752
753        // Step 3.
754        if !JS_DefineProperty4(
755            &mut cx,
756            Handle::from_raw(meta_object),
757            c"url".as_ptr(),
758            url_string.handle(),
759            JSPROP_ENUMERATE.into(),
760        ) {
761            return false;
762        }
763
764        // Step 5. Let resolveFunction be ! CreateBuiltinFunction(steps, 1, "resolve", « »).
765        let resolve_function = DefineFunctionWithReserved(
766            &mut cx,
767            meta_object.get(),
768            c"resolve".as_ptr(),
769            Some(import_meta_resolve),
770            1,
771            JSPROP_ENUMERATE.into(),
772        );
773
774        if resolve_function.is_null() {
775            return false;
776        }
777
778        rooted!(&in(cx) let obj = JS_GetFunctionObject(resolve_function));
779        assert!(!obj.is_null());
780        SetFunctionNativeReserved(
781            obj.get(),
782            MODULE_RECORD_SLOT,
783            &ObjectValue(module_record.get()),
784        );
785    }
786
787    true
788}
789
790#[expect(unsafe_code)]
791unsafe extern "C" fn import_meta_resolve(cx: *mut RawJSContext, argc: u32, vp: *mut JSVal) -> bool {
792    // SAFETY: it is safe to construct a JSContext from engine hook.
793    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
794    let mut realm = CurrentRealm::assert(&mut cx);
795    let global_scope = GlobalScope::from_current_realm(&mut realm);
796
797    let cx = &mut realm;
798
799    let args = unsafe { CallArgs::from_vp(vp, argc) };
800
801    rooted!(&in(cx) let module_value = unsafe { *GetFunctionNativeReserved(args.callee(), MODULE_RECORD_SLOT) });
802    assert!(!module_value.is_undefined());
803    rooted!(&in(cx) let module_record = module_value.to_object());
804    rooted!(&in(cx) let mut module_private: JSVal);
805    unsafe { JS_GetModulePrivate(module_record.get(), module_private.handle_mut()) };
806    assert!(!module_private.is_undefined());
807    let module_data = unsafe { module_script_from_reference_private(module_private.handle()) };
808
809    // https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties
810
811    // Step 4.1. Set specifier to ? ToString(specifier).
812    let specifier = unsafe {
813        let value = HandleValue::from_raw(args.get(0));
814
815        match NonNull::new(ToString(cx, value)) {
816            Some(jsstr) => jsstr_to_string(cx, jsstr),
817            None => return false,
818        }
819    };
820
821    // Step 4.2. Let url be the result of resolving a module specifier given moduleScript and specifier.
822    let url = ImportMap::resolve_module_specifier(&global_scope, module_data, specifier);
823
824    match url {
825        Ok(url) => {
826            // Step 4.3. Return the serialization of url.
827            url.as_str()
828                .to_jsval(cx, unsafe { MutableHandleValue::from_raw(args.rval()) });
829            true
830        },
831        Err(error) => {
832            throw_dom_exception(cx, &global_scope, error);
833            false
834        },
835    }
836}
837
838#[expect(clippy::too_many_arguments)]
839/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree>
840/// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet/module-worker-script-graph>
841pub(crate) fn fetch_a_module_script_graph(
842    cx: &mut JSContext,
843    global: &GlobalScope,
844    url: UrlWithBlobClaim,
845    fetch_client: RequestClient,
846    destination: Destination,
847    referrer: Referrer,
848    credentials_mode: CredentialsMode,
849    introduction_type: Option<&'static CStr>,
850    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
851) {
852    let global_scope = DomRoot::from_ref(global);
853
854    // Step 1. Let options be a script fetch options whose cryptographic nonce
855    // is the empty string, integrity metadata is the empty string, parser
856    // metadata is "not-parser-inserted", credentials mode is credentialsMode,
857    // referrer policy is the empty string, and fetch priority is "auto".
858    let options = ScriptFetchOptions {
859        integrity_metadata: String::new(),
860        credentials_mode,
861        cryptographic_nonce: String::new(),
862        parser_metadata: ParserMetadata::NotParserInserted,
863        referrer_policy: ReferrerPolicy::EmptyString,
864        render_blocking: false,
865    };
866
867    // Step 2. Fetch a single module script given url, fetchClient, destination, options,
868    // settingsObject, "client", true, and onSingleFetchComplete as defined below.
869    fetch_a_single_module_script(
870        cx,
871        url,
872        fetch_client.clone(),
873        global,
874        destination,
875        options,
876        referrer,
877        None,
878        true,
879        introduction_type,
880        move |cx, module_tree| {
881            let Some(module) = module_tree else {
882                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
883                return on_complete(cx, None);
884            };
885
886            // Step 1.2. Fetch the descendants of and link result given fetchClient, destination,
887            // and onComplete.
888            fetch_the_descendants_and_link_module_script(
889                cx,
890                &global_scope,
891                module,
892                fetch_client,
893                destination,
894                on_complete,
895            );
896        },
897    );
898}
899
900/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-script-tree>
901pub(crate) fn fetch_an_external_module_script(
902    cx: &mut JSContext,
903    url: UrlWithBlobClaim,
904    global: &GlobalScope,
905    options: ScriptFetchOptions,
906    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
907) {
908    let referrer = global.get_referrer();
909    let fetch_client = global.request_client(Some(cx.no_gc()));
910    let global_scope = DomRoot::from_ref(global);
911
912    // Step 1. Fetch a single module script given url, settingsObject, "script", options, settingsObject, "client", true,
913    // and with the following steps given result:
914    fetch_a_single_module_script(
915        cx,
916        url,
917        fetch_client.clone(),
918        global,
919        Destination::Script,
920        options,
921        referrer,
922        None,
923        true,
924        Some(IntroductionType::SRC_SCRIPT),
925        move |cx, module_tree| {
926            let Some(module) = module_tree else {
927                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
928                return on_complete(cx, None);
929            };
930
931            // Step 1.2. Fetch the descendants of and link result given settingsObject, "script", and onComplete.
932            fetch_the_descendants_and_link_module_script(
933                cx,
934                &global_scope,
935                module,
936                fetch_client,
937                Destination::Script,
938                on_complete,
939            );
940        },
941    );
942}
943
944/// <https://html.spec.whatwg.org/multipage/#fetch-a-modulepreload-module-script-graph>
945pub(crate) fn fetch_a_modulepreload_module(
946    cx: &mut JSContext,
947    url: UrlWithBlobClaim,
948    destination: Destination,
949    global: &GlobalScope,
950    options: ScriptFetchOptions,
951    on_complete: impl FnOnce(&mut JSContext, bool) + 'static,
952) {
953    let referrer = global.get_referrer();
954    let fetch_client = global.request_client(Some(cx.no_gc()));
955    let global_scope = DomRoot::from_ref(global);
956
957    // Note: There is a specification inconsistency, `fetch_a_single_module_script` doesn't allow
958    // fetching top level JSON/CSS/Text module scripts, but should be possible when preloading.
959    let module_type = match destination {
960        Destination::Json => Some(ModuleType::JSON),
961        Destination::Style => Some(ModuleType::CSS),
962        Destination::Text => Some(ModuleType::Text),
963        _ => None,
964    };
965
966    // Step 1. Fetch a single module script given url, settingsObject, destination, options, settingsObject,
967    // "client", true, and with the following steps given result:
968    fetch_a_single_module_script(
969        cx,
970        url,
971        fetch_client.clone(),
972        global,
973        destination,
974        options,
975        referrer,
976        module_type,
977        true,
978        Some(IntroductionType::SRC_SCRIPT),
979        move |cx, result| {
980            // Step 1. Run onComplete given result.
981            on_complete(cx, result.is_none());
982
983            // Step 2. Assert: settingsObject's global object implements Window.
984            assert!(global_scope.is::<Window>());
985
986            // Step 3. If result is not null, optionally fetch the descendants of and link result
987            // given settingsObject, destination, and an empty algorithm.
988            if pref!(dom_allow_preloading_module_descendants) &&
989                let Some(module) = result
990            {
991                fetch_the_descendants_and_link_module_script(
992                    cx,
993                    &global_scope,
994                    module,
995                    fetch_client,
996                    destination,
997                    |_, _| {},
998                );
999            }
1000        },
1001    );
1002}
1003
1004#[expect(clippy::too_many_arguments)]
1005/// <https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph>
1006pub(crate) fn fetch_inline_module_script(
1007    cx: &mut JSContext,
1008    global: &GlobalScope,
1009    module_script_text: Cow<'_, str>,
1010    url: ServoUrl,
1011    options: ScriptFetchOptions,
1012    line_number: u32,
1013    introduction_type: Option<&'static CStr>,
1014    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1015) {
1016    let mut realm = enter_auto_realm(cx, global);
1017    let cx = &mut realm.current_realm();
1018
1019    // Step 1. Let script be the result of creating a JavaScript module script using sourceText, settingsObject, baseURL, and options.
1020    let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
1021        cx,
1022        module_script_text,
1023        global,
1024        &url,
1025        options,
1026        false,
1027        line_number,
1028        introduction_type,
1029    ));
1030    let fetch_client = global.request_client(Some(cx.no_gc()));
1031
1032    // Step 2. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
1033    fetch_the_descendants_and_link_module_script(
1034        cx,
1035        global,
1036        module_tree,
1037        fetch_client,
1038        Destination::Script,
1039        on_complete,
1040    );
1041}
1042
1043/// <https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script>
1044fn fetch_the_descendants_and_link_module_script(
1045    cx: &mut JSContext,
1046    global: &GlobalScope,
1047    module_script: Rc<ModuleTree>,
1048    fetch_client: RequestClient,
1049    destination: Destination,
1050    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1051) {
1052    // Step 1. Let record be moduleScript's record.
1053    // Step 2. If record is null, then:
1054    let Some(record) = module_script.get_record() else {
1055        let parse_error = module_script.get_parse_error().cloned();
1056
1057        // Step 2.1. Set moduleScript's error to rethrow to moduleScript's parse error.
1058        module_script.set_rethrow_error(parse_error.unwrap());
1059
1060        // Step 2.2. Run onComplete given moduleScript.
1061        on_complete(cx, Some(module_script));
1062
1063        // Step 2.3. Return.
1064        return;
1065    };
1066
1067    // Step 3. Let state be Record
1068    // { [[ErrorToRethrow]]: null, [[Destination]]: destination, [[PerformFetch]]: null, [[FetchClient]]: fetchClient }.
1069    let state = Box::new(LoadState {
1070        destination,
1071        fetch_client,
1072        module_script: DomRefCell::new(Some(module_script.clone())),
1073        on_complete: DomRefCell::new(Some(Box::new(on_complete))),
1074    });
1075
1076    // TODO Step 4. If performFetch was given, set state.[[PerformFetch]] to performFetch.
1077
1078    let mut realm = enter_auto_realm(cx, global);
1079    let cx = &mut realm.current_realm();
1080
1081    // Step 5. Let loadingPromise be record.LoadRequestedModules(state).
1082    load_requested_modules(cx, record.handle(), state);
1083}
1084
1085/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
1086#[expect(clippy::too_many_arguments)]
1087pub(crate) fn fetch_a_single_module_script(
1088    cx: &mut JSContext,
1089    url: UrlWithBlobClaim,
1090    fetch_client: RequestClient,
1091    global: &GlobalScope,
1092    destination: Destination,
1093    options: ScriptFetchOptions,
1094    referrer: Referrer,
1095    module_type: Option<ModuleType>,
1096    is_top_level: bool,
1097    introduction_type: Option<&'static CStr>,
1098    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
1099) {
1100    // Step 1. Let moduleType be "javascript-or-wasm".
1101    // Step 2. If moduleRequest was given, then set moduleType to the result of running the
1102    // module type from module request steps given moduleRequest.
1103    let module_type = module_type.unwrap_or(ModuleType::JavaScript);
1104
1105    // TODO Step 3. Assert: the result of running the module type allowed steps given moduleType and settingsObject is true.
1106    // Otherwise, we would not have reached this point because a failure would have been raised
1107    // when inspecting moduleRequest.[[Attributes]] in HostLoadImportedModule or fetch a single imported module script.
1108
1109    let module_request = (url.url(), module_type);
1110
1111    // Step 4. Let moduleMap be settingsObject's module map.
1112    let occupied = global.with_module_map(|module_map| {
1113        let mut module_map_borrow = module_map.safe_borrow_mut(cx);
1114
1115        let entry = module_map_borrow.entry(module_request.clone());
1116
1117        match entry {
1118            Entry::Occupied(mut entry) => {
1119                match entry.get_mut() {
1120                    // Step 5. If moduleMap[(url, moduleType)] is a module script, run onComplete given
1121                    // moduleMap[(url, moduleType)], and return.
1122                    ModuleStatus::Loaded(module_tree) => {
1123                        let module = module_tree.clone();
1124                        drop(module_map_borrow);
1125                        on_complete(cx, Some(module));
1126                    },
1127                    // Step 6. If moduleMap[(url, moduleType)] is a list, append onComplete to
1128                    // moduleMap[(url, moduleType)], and return.
1129                    ModuleStatus::Fetching(callbacks) => {
1130                        callbacks.push(Box::new(on_complete));
1131                    },
1132                }
1133                true
1134            },
1135            // Step 7. Set moduleMap[(url, moduleType)] to « onComplete ».
1136            Entry::Vacant(entry) => {
1137                entry.insert(ModuleStatus::Fetching(vec![Box::new(on_complete)]));
1138                false
1139            },
1140        }
1141    });
1142
1143    if occupied {
1144        return;
1145    }
1146
1147    // We only need a policy container when fetching the root of a module worker.
1148    let policy_container = (is_top_level && global.is::<WorkerGlobalScope>())
1149        .then(|| fetch_client.policy_container.clone());
1150
1151    // Step 8. Let request be a new request whose URL is url, mode is "cors", referrer is referrer, and client is fetchClient.
1152
1153    // Step 10. If destination is "worker", "sharedworker", or "serviceworker", and isTopLevel is true,
1154    // then set request's mode to "same-origin".
1155    let mode = match destination {
1156        Destination::Worker | Destination::SharedWorker if is_top_level => RequestMode::SameOrigin,
1157        _ => RequestMode::CorsMode,
1158    };
1159
1160    // Step 9. Set request's destination to the result of running the
1161    // fetch destination from module type steps given destination and moduleType.
1162    let destination = match module_type {
1163        ModuleType::JSON => Destination::Json,
1164        ModuleType::CSS => Destination::Style,
1165        ModuleType::Text => Destination::Text,
1166        ModuleType::Bytes => unreachable!("Not in ESR153"),
1167        ModuleType::JavaScript | ModuleType::Unknown => destination,
1168    };
1169
1170    // TODO Step 11. Set request's initiator type to "script".
1171
1172    // Step 12. Set up the module script request given request and options.
1173    let request = RequestBuilder::new(global.webview_id(), url, referrer)
1174        .destination(destination)
1175        .parser_metadata(options.parser_metadata)
1176        .integrity_metadata(options.integrity_metadata.clone())
1177        .credentials_mode(options.credentials_mode)
1178        .referrer_policy(options.referrer_policy)
1179        .mode(mode)
1180        .cryptographic_nonce_metadata(options.cryptographic_nonce.clone())
1181        .client(fetch_client)
1182        .pipeline_id(Some(global.pipeline_id()));
1183
1184    let context = ModuleContext {
1185        owner: Trusted::new(global),
1186        data: BytesMut::new(),
1187        metadata: None,
1188        module_request,
1189        options,
1190        status: Ok(()),
1191        introduction_type,
1192        policy_container,
1193    };
1194
1195    let task_source = global.task_manager().networking_task_source().to_sendable();
1196    global.fetch(request, context, task_source);
1197}
1198
1199/// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record>
1200#[derive(Default, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
1201pub(crate) struct ResolvedModule {
1202    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-serialized-base-url>
1203    pub(crate) base_url: String,
1204    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-specifier>
1205    pub(crate) specifier: String,
1206    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-as-url>
1207    #[no_trace]
1208    pub(crate) specifier_url: Option<ServoUrl>,
1209}
1210
1211impl ResolvedModule {
1212    pub(crate) fn new(
1213        base_url: String,
1214        specifier: String,
1215        specifier_url: Option<ServoUrl>,
1216    ) -> Self {
1217        Self {
1218            base_url,
1219            specifier,
1220            specifier_url,
1221        }
1222    }
1223}