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