#![expect(unsafe_code)]
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
use js::context::JSContext;
use js::conversions::jsstr_to_string;
use js::jsapi::{HandleValue as RawHandleValue, IsCyclicModule, ModuleType};
use js::jsval::{ObjectValue, UndefinedValue};
use js::realm::{AutoRealm, CurrentRealm};
use js::rust::wrappers2::{
GetModuleNamespace, GetRequestedModuleSpecifier, GetRequestedModuleType,
GetRequestedModulesCount, JS_GetModulePrivate, ModuleEvaluate, ModuleLink,
};
use js::rust::{HandleValue, IntoHandle};
use net_traits::blob_url_store::UrlWithBlobClaim;
use net_traits::request::{Destination, Referrer, RequestClient};
use script_bindings::reflector::DomObject;
use script_bindings::settings_stack::run_a_callback;
use servo_url::ServoUrl;
use crate::DomTypeHolder;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::modules::script_module::{
ModuleHandler, ModuleObject, ModuleTree, RethrowError, ScriptFetchOptions,
fetch_a_single_module_script, gen_type_error, module_script_from_reference_private,
};
use crate::realms::enter_auto_realm;
use crate::script_runtime::IntroductionType;
use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
#[derive(JSTraceable, MallocSizeOf)]
struct OnRejectedHandler {
#[conditional_malloc_size_of]
promise: Rc<Promise>,
}
impl Callback for OnRejectedHandler {
fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
self.promise.reject(cx, v);
}
}
pub(crate) enum Payload {
GraphRecord(Rc<GraphLoadingState>),
PromiseRecord(Rc<Promise>),
}
#[derive(JSTraceable)]
pub(crate) struct LoadState {
pub(crate) error_to_rethrow: RefCell<Option<RethrowError>>,
#[no_trace]
pub(crate) destination: Destination,
#[no_trace]
pub(crate) fetch_client: RequestClient,
}
pub(crate) struct GraphLoadingState {
promise: Rc<Promise>,
is_loading: Cell<bool>,
pending_modules_count: Cell<u32>,
visited: RefCell<HashSet<ServoUrl>>,
load_state: Option<Rc<LoadState>>,
}
pub(crate) fn load_requested_modules(
cx: &mut CurrentRealm,
module: Rc<ModuleTree>,
load_state: Option<Rc<LoadState>>,
) -> Rc<Promise> {
let mut realm = CurrentRealm::assert(cx);
let promise = Promise::new_in_realm(&mut realm);
let state = GraphLoadingState {
promise: promise.clone(),
is_loading: Cell::new(true),
pending_modules_count: Cell::new(1),
visited: RefCell::new(HashSet::new()),
load_state,
};
inner_module_loading(cx, &Rc::new(state), module);
promise
}
fn inner_module_loading(
cx: &mut CurrentRealm,
state: &Rc<GraphLoadingState>,
module: Rc<ModuleTree>,
) {
assert!(state.is_loading.get());
let module_handle = module.get_record().map(|module| module.handle()).unwrap();
let module_url = module.get_url();
let visited_contains_module = state.visited.borrow().contains(&module_url);
if unsafe { IsCyclicModule(module_handle.get()) } && !visited_contains_module {
state.visited.borrow_mut().insert(module_url);
let requested_modules_count = unsafe { GetRequestedModulesCount(cx, module_handle) };
let pending_modules_count = state.pending_modules_count.get();
state
.pending_modules_count
.set(pending_modules_count + requested_modules_count);
for index in 0..requested_modules_count {
let jsstr = unsafe { GetRequestedModuleSpecifier(cx, module_handle, index) };
if jsstr.is_null() {
let error = RethrowError::from_pending_exception(cx);
state.load_state.as_ref().inspect(|load_state| {
load_state
.error_to_rethrow
.borrow_mut()
.get_or_insert(error.clone());
});
continue_module_loading(cx, state, Err(error));
} else {
let specifier =
unsafe { jsstr_to_string(cx, std::ptr::NonNull::new(jsstr).unwrap()) };
let module_type = unsafe { GetRequestedModuleType(cx, module_handle, index) };
let mut realm = CurrentRealm::assert(cx);
let global = GlobalScope::from_current_realm(&mut realm);
let loaded_module =
module.find_descendant_inside_module_map(&global, &specifier, module_type);
match loaded_module {
Some(module) => inner_module_loading(cx, state, module),
None => {
rooted!(&in(cx) let mut referrer = UndefinedValue());
unsafe { JS_GetModulePrivate(module_handle.get(), referrer.handle_mut()) };
host_load_imported_module(
cx,
Some(module.clone()),
referrer.handle().into_handle(),
specifier,
module_type,
state.load_state.clone(),
Payload::GraphRecord(state.clone()),
);
},
}
}
if !state.is_loading.get() {
return;
}
}
}
assert!(state.pending_modules_count.get() >= 1);
let pending_modules_count = state.pending_modules_count.get();
state.pending_modules_count.set(pending_modules_count - 1);
if state.pending_modules_count.get() == 0 {
state.is_loading.set(false);
state.promise.resolve_native(cx, &());
}
}
fn continue_module_loading(
cx: &mut CurrentRealm,
state: &Rc<GraphLoadingState>,
module_completion: Result<Rc<ModuleTree>, RethrowError>,
) {
if !state.is_loading.get() {
return;
}
match module_completion {
Ok(module) => inner_module_loading(cx, state, module),
Err(exception) => {
state.is_loading.set(false);
state.promise.reject(cx, exception.handle());
},
}
}
fn finish_loading_imported_module(
cx: &mut CurrentRealm,
referrer_module: Option<Rc<ModuleTree>>,
module_request_specifier: String,
payload: Payload,
result: Result<Rc<ModuleTree>, RethrowError>,
) {
match payload {
Payload::GraphRecord(state) => {
let module_tree =
referrer_module.expect("Module must not be None in non dynamic imports");
if let Ok(ref module) = result {
module_tree.insert_module_dependency(module, module_request_specifier);
}
continue_module_loading(cx, &state, result);
},
Payload::PromiseRecord(promise) => continue_dynamic_import(cx, promise, result),
}
}
fn continue_dynamic_import(
realm: &mut CurrentRealm,
promise: Rc<Promise>,
module_completion: Result<Rc<ModuleTree>, RethrowError>,
) {
if let Err(exception) = module_completion {
promise.reject(realm, exception.handle());
return;
}
let global = GlobalScope::from_current_realm(realm);
let module = module_completion.unwrap();
let record = ModuleObject::new(module.get_record().map(|module| module.handle()).unwrap());
let load_promise = load_requested_modules(realm, module, None);
let global_scope = global.clone();
let inner_promise = promise.clone();
let fulfilled_promise = promise.clone();
let link_and_evaluate = ModuleHandler::new_boxed(Box::new(
task!(link_and_evaluate: |cx, global_scope: DomRoot<GlobalScope>, inner_promise: Rc<Promise>, record: ModuleObject| {
let mut realm = AutoRealm::new(
cx,
std::ptr::NonNull::new(global_scope.reflector().get_jsobject().get()).unwrap(),
);
let cx = &mut realm.current_realm();
let link = unsafe { ModuleLink(cx, record.handle()) };
if !link {
let exception = RethrowError::from_pending_exception(cx);
inner_promise.reject(cx, exception.handle());
return;
}
rooted!(&in(cx) let mut rval = UndefinedValue());
assert!(unsafe { ModuleEvaluate(cx, record.handle(), rval.handle_mut()) });
if !rval.is_object() {
let error = RethrowError::from_pending_exception(cx);
return inner_promise.reject(cx, error.handle());
}
rooted!(&in(cx) let evaluate_promise = rval.to_object());
let evaluate_promise = Promise::new_with_js_promise(cx, evaluate_promise.handle());
let on_fulfilled = ModuleHandler::new_boxed(Box::new(
task!(on_fulfilled: |cx, fulfilled_promise: Rc<Promise>, record: ModuleObject| {
rooted!(&in(cx) let rval = unsafe { GetModuleNamespace(cx, record.handle()) });
rooted!(&in(cx) let namespace = ObjectValue(rval.get()));
fulfilled_promise.resolve(cx, namespace.handle());
})));
let handler = PromiseNativeHandler::new(cx, &global_scope,
Some(on_fulfilled),
Some(Box::new(OnRejectedHandler { promise: inner_promise })));
evaluate_promise.append_native_handler(cx, &handler);
}),
));
let mut realm = enter_auto_realm(realm, &*global);
let cx = &mut realm.current_realm();
run_a_callback::<DomTypeHolder, _>(&*global, || {
let handler = PromiseNativeHandler::new(
cx,
&global,
Some(link_and_evaluate),
Some(Box::new(OnRejectedHandler { promise })),
);
load_promise.append_native_handler(cx, &handler);
});
}
pub(crate) fn host_load_imported_module(
cx: &mut CurrentRealm,
referrer_module: Option<Rc<ModuleTree>>,
referrer: RawHandleValue,
specifier: String,
module_type: ModuleType,
load_state: Option<Rc<LoadState>>,
payload: Payload,
) {
let mut realm = CurrentRealm::assert(cx);
let mut global_scope = GlobalScope::from_current_realm(&mut realm);
let referencing_script = unsafe { module_script_from_reference_private(&referrer) };
let (original_fetch_options, fetch_referrer) = match referencing_script {
Some(module) => (
module.options.clone(),
Referrer::ReferrerUrl(module.base_url.clone()),
),
None => (
ScriptFetchOptions::default_classic_script(),
global_scope.get_referrer(),
),
};
let script_owner = referencing_script.and_then(|script| script.owner.clone());
if let Some(ref owner) = script_owner {
global_scope = owner.root();
}
let global = &global_scope.clone();
let url =
ModuleTree::resolve_module_specifier(global, referencing_script, specifier.clone().into());
if let Err(error) = url {
let resolution_error = gen_type_error(cx, &global_scope, error);
load_state.as_ref().inspect(|load_state| {
load_state
.error_to_rethrow
.borrow_mut()
.get_or_insert(resolution_error.clone());
});
finish_loading_imported_module(
cx,
referrer_module,
specifier,
payload,
Err(resolution_error),
);
return;
};
let url = ensure_blob_referenced_by_url_is_kept_alive(global, url.unwrap());
let fetch_options = original_fetch_options.descendant_fetch_options(&url.url(), &global_scope);
let (destination, fetch_client) = match load_state.as_ref() {
Some(load_state) => (load_state.destination, load_state.fetch_client.clone()),
None => (
Destination::Script,
global_scope.request_client(Some(cx.no_gc())),
),
};
let on_single_fetch_complete =
move |cx: &mut JSContext, module_tree: Option<Rc<ModuleTree>>| {
let mut realm = CurrentRealm::assert(cx);
let cx = &mut realm;
let completion = match module_tree {
None => Err(gen_type_error(
cx,
&global_scope,
Error::Type(c"Module fetching failed".to_owned()),
)),
Some(module_tree) => {
if let Some(parse_error) = module_tree.get_parse_error() {
load_state.as_ref().inspect(|load_state| {
load_state
.error_to_rethrow
.borrow_mut()
.get_or_insert(parse_error.clone());
});
Err(parse_error.clone())
} else {
Ok(module_tree)
}
},
};
finish_loading_imported_module(cx, referrer_module, specifier, payload, completion);
};
fetch_a_single_imported_module_script(
cx,
url,
fetch_client,
global,
destination,
fetch_options,
fetch_referrer,
module_type,
on_single_fetch_complete,
);
}
#[expect(clippy::too_many_arguments)]
fn fetch_a_single_imported_module_script(
cx: &mut JSContext,
url: UrlWithBlobClaim,
fetch_client: RequestClient,
global: &GlobalScope,
destination: Destination,
options: ScriptFetchOptions,
referrer: Referrer,
module_type: ModuleType,
on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
) {
match module_type {
ModuleType::Unknown => return on_complete(cx, None),
ModuleType::JavaScript | ModuleType::JSON => (),
}
fetch_a_single_module_script(
cx,
url,
fetch_client,
global,
destination,
options,
referrer,
Some(module_type),
false,
Some(IntroductionType::IMPORTED_MODULE),
on_complete,
);
}