// Copyright 2018-2026 the Deno authors. MIT license. use std::cell::Cell; use std::cell::RefCell; use std::collections::HashMap; use std::collections::VecDeque; use std::rc::Rc; use std::task::Waker; use deno_core::OpState; use deno_core::op2; use deno_core::v8; use deno_error::JsErrorBox; /// A pending load request from the Rust module loader to JS hooks. struct PendingLoad { id: u32, url: String, } /// Load hook result: (source, format). Format is e.g. "commonjs", "module". type LoadResult = (Option, Option); type LoadSender = deno_core::futures::channel::oneshot::Sender>; /// Callback used to perform the default ESM resolution from JS hooks. /// Installed by the embedder so that the JS terminal `nextResolve` fallback /// can reach the real module loader (handling bare specifiers, package /// exports, import maps, npm/jsr, etc.) the same way an un-hooked import /// would. pub type DefaultResolveCb = Rc Result>; /// Shared hook registry between ops and the module loader. /// /// When load hooks are active, the Rust module loader pushes requests into /// the pending queue. The JS side polls for requests via an async op, calls /// the user's synchronous hook function, and sends the response back via a /// sync op. #[derive(Clone, Default)] pub struct LoaderHookRegistry { resolve_callback: Rc>>>, pub load_active: Rc>, next_id: Rc>, pending_loads: Rc>>, load_waker: Rc>>, load_senders: Rc>>, /// Maps load request ID to URL for dedup tracking. load_id_keys: Rc>>, /// Piggybacking senders for duplicate load requests. load_waiters: Rc>>>, default_resolve: Rc>>, } impl LoaderHookRegistry { fn next_id(&self) -> u32 { let id = self.next_id.get(); self.next_id.set(id + 1); id } /// Install the default-resolution callback used by the JS hook chain when /// the terminal `nextResolve` is reached. The embedder is expected to /// provide a function that performs the same resolution as a normal /// (un-hooked) import. pub fn set_default_resolve(&self, cb: DefaultResolveCb) { *self.default_resolve.borrow_mut() = Some(cb); } /// Call the default-resolution callback. Used by /// `op_module_default_resolve`. pub fn default_resolve( &self, specifier: &str, referrer: &str, ) -> Result { let cb = self.default_resolve.borrow().clone(); match cb { Some(cb) => cb(specifier, referrer), None => Err(JsErrorBox::generic( "default module resolver is not available", )), } } pub fn resolve( &self, scope: &mut v8::PinScope, specifier: &str, referrer: &str, ) -> Result, JsErrorBox> { let callbacks = self.resolve_callback.borrow(); let Some(callback) = callbacks.as_ref() else { return Ok(None); }; let callback = v8::Local::new(scope, callback); let recv = v8::undefined(scope).into(); let specifier = v8::String::new(scope, specifier) .ok_or_else(|| JsErrorBox::generic("failed to allocate specifier"))?; let referrer = v8::String::new(scope, referrer) .ok_or_else(|| JsErrorBox::generic("failed to allocate referrer"))?; let Some(result) = callback.call(scope, recv, &[specifier.into(), referrer.into()]) else { return Err(JsErrorBox::generic("module resolve hook failed")); }; if result.is_null_or_undefined() { return Ok(None); } if result.is_string() { let result = v8::Local::::try_from(result) .map_err(|_| JsErrorBox::generic("module resolve hook failed"))?; return Ok(Some(result.to_rust_string_lossy(scope))); } if let Ok(result) = v8::Local::::try_from(result) { let error_key = v8::String::new(scope, "error") .ok_or_else(|| JsErrorBox::generic("failed to allocate error key"))?; if let Some(error) = result.get(scope, error_key.into()) && !error.is_null_or_undefined() { let error = error .to_string(scope) .ok_or_else(|| JsErrorBox::generic("module resolve hook failed"))?; return Err(JsErrorBox::generic(error.to_rust_string_lossy(scope))); } } Err(JsErrorBox::generic( "module resolve hook must return a string or null", )) } /// Push a load request and return a receiver for the response. /// `Ok((Some(source), format))` = hook provided source, /// `Ok((None, _))` = fallthrough. pub fn push_load( &self, url: String, ) -> deno_core::futures::channel::oneshot::Receiver> { // Dedup: if there's already a pending load for this URL, piggyback. if self.load_waiters.borrow().contains_key(&url) { let (sender, receiver) = deno_core::futures::channel::oneshot::channel(); self .load_waiters .borrow_mut() .get_mut(&url) .unwrap() .push(sender); return receiver; } self .load_waiters .borrow_mut() .insert(url.clone(), Vec::new()); let id = self.next_id(); let (sender, receiver) = deno_core::futures::channel::oneshot::channel(); self.load_senders.borrow_mut().insert(id, sender); self.load_id_keys.borrow_mut().insert(id, url.clone()); self .pending_loads .borrow_mut() .push_back(PendingLoad { id, url }); if let Some(waker) = self.load_waker.borrow_mut().take() { waker.wake(); } receiver } } /// Mark hooks as active. Called from JS when `registerHooks()` is invoked. #[op2] pub fn op_module_hooks_register( state: &mut OpState, #[scoped] resolve_callback: Option>, has_load: bool, ) { let registry = state.borrow::().clone(); *registry.resolve_callback.borrow_mut() = resolve_callback; registry.load_active.set(has_load); } /// Poll for a pending load request. Returns `[id, url]` or null. #[op2] #[serde] pub async fn op_module_hooks_poll_load( state: Rc>, ) -> Result, JsErrorBox> { let registry = state.borrow().borrow::().clone(); std::future::poll_fn(|cx| { if let Some(req) = registry.pending_loads.borrow_mut().pop_front() { return std::task::Poll::Ready(Ok(Some((req.id, req.url)))); } *registry.load_waker.borrow_mut() = Some(cx.waker().clone()); std::task::Poll::Pending }) .await } /// Run the default module resolver. Used by the JS hook chain's terminal /// `nextResolve` so that hooks observing the default resolution see the real /// URL that Deno would have resolved (bare specifiers, package exports, /// import maps, npm/jsr, etc.) rather than a stub. #[op2] #[string] pub fn op_module_default_resolve( state: &mut OpState, #[string] specifier: &str, #[string] referrer: &str, ) -> Result { let registry = state.borrow::().clone(); registry.default_resolve(specifier, referrer) } /// Respond to a load request. `source` is null to delegate to default loading. #[op2] pub fn op_module_hooks_respond_load( state: &mut OpState, id: u32, #[string] source: Option, #[string] format: Option, #[string] error: Option, ) { let registry = state.borrow::().clone(); let result: Result = if let Some(err) = error { Err(err) } else { Ok((source, format)) }; // Fulfill piggybacking waiters. if let Some(key) = registry.load_id_keys.borrow_mut().remove(&id) && let Some(waiters) = registry.load_waiters.borrow_mut().remove(&key) { for waiter in waiters { let _ = waiter.send(result.clone()); } } if let Some(sender) = registry.load_senders.borrow_mut().remove(&id) { let _ = sender.send(result); } }