/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/core/modules/map.rs
4 192 строки
140 KB
Nathan Whitaker
fix(core): don't resolve internal module imports with the user's import map (#36303)
27 июл 2026, 11:10
Не верифицирован
27 июл 2026, 11:10
985d218
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::Cell; use std::cell::RefCell; use std::collections::HashMap; use std::future::Future; use std::ops::DerefMut; use std::pin::Pin; use std::rc::Rc; use std::task::Context; use std::task::Poll; use capacity_builder::StringBuilder; use deno_core::FastString; use deno_core::error::CoreError; use deno_error::JsErrorBox; use futures::StreamExt; use futures::future::Either; use futures::future::FutureExt; use futures::stream::FuturesUnordered; use futures::stream::StreamFuture; use futures::task::AtomicWaker; use indexmap::IndexMap; use sourcemap::DecodedMap; use tokio::sync::oneshot; use v8::Function; use v8::PromiseState; use wasm_dep_analyzer::WasmDeps; use super::CustomModuleEvaluationKind; use super::ImportAttributesContext; use super::IntoModuleCodeString; use super::IntoModuleName; use super::LazyEsmModuleLoader; use super::ModuleConcreteError; use super::RequestedModuleType; use super::loaders::ModuleLoadOptions; use super::module_map_data::ModuleMapData; use super::module_map_data::ModuleMapSnapshotData; use crate::FastStaticString; use crate::JsRuntime; use crate::ModuleCodeBytes; use crate::ModuleLoadResponse; use crate::ModuleResolveResponse; use crate::ModuleSource; use crate::ModuleSourceCode; use crate::ModuleSpecifier; use crate::ascii_str; use crate::error::CoreErrorKind; use crate::error::JsError; use crate::error::exception_to_err; use crate::error::exception_to_err_result; use crate::modules::ImportAttributesKind; use crate::modules::ModuleCodeString; use crate::modules::ModuleError; use crate::modules::ModuleId; use crate::modules::ModuleImportPhase; use crate::modules::ModuleLoadId; use crate::modules::ModuleLoader; use crate::modules::ModuleName; use crate::modules::ModuleReference; use crate::modules::ModuleRequest; use crate::modules::ModuleType; use crate::modules::ResolutionKind; use crate::modules::get_requested_module_type_from_attributes; use crate::modules::module_map_data::ModuleSourceKey; use crate::modules::parse_import_attributes; use crate::modules::recursive_load::RecursiveModuleLoad; use crate::modules::recursive_load::RegisterOutcome; use crate::runtime::JsRealm; use crate::runtime::SnapshotLoadDataStore; use crate::runtime::SnapshotStoreDataStore; use crate::runtime::exception_state::ExceptionState; use crate::source_map::SourceMapper; const DATA_PREFIX: &str = "data:"; fn is_internal_scheme(scheme: &str) -> bool { matches!(scheme, "ext" | "node" | "checkin") } fn is_internal_module_specifier(specifier: &str) -> bool { let Ok(specifier) = ModuleSpecifier::parse(specifier) else { return false; }; is_internal_scheme(specifier.scheme()) } fn residual_source_from_static_table( entries: &'static [(&'static str, &'static str)], specifier: &str, ) -> Option<ModuleCodeString> { entries .binary_search_by(|(entry_specifier, _)| entry_specifier.cmp(&specifier)) .ok() .map(|index| { let code = entries[index].1; // SAFETY: Residual sources are generated by the snapshot build and // asserted to be ASCII there. unsafe { ModuleCodeString::from_ascii_static_unchecked(code) } }) } fn residual_static_table_contains( entries: &'static [(&'static str, &'static str)], specifier: &str, ) -> bool { entries .binary_search_by(|(entry_specifier, _)| entry_specifier.cmp(&specifier)) .is_ok() } fn debug_assert_residual_static_table_sorted( entries: &'static [(&'static str, &'static str)], ) { debug_assert!( entries.windows(2).all(|window| window[0].0 < window[1].0), "residual lazy source tables must be sorted by specifier", ); } type PrepareLoadFuture = dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, CoreError>)>; type CodeCacheReadyFuture = dyn Future<Output = ()>; struct ModEvaluate { module_map: Rc<ModuleMap>, sender: Option<oneshot::Sender<Result<(), Box<JsError>>>>, module: Option<v8::Global<v8::Module>>, notify: Vec<v8::Global<v8::Function>>, } impl ModEvaluate { fn notify(&mut self, scope: &mut v8::PinScope) { if !self.notify.is_empty() { let module = v8::Local::new(scope, self.module.take().unwrap()); let ns = module.get_module_namespace(); let recv = v8::undefined(scope).into(); let args = &[ns]; for notify in std::mem::take(&mut self.notify).into_iter() { let notify = v8::Local::new(scope, notify); notify.call(scope, recv, args); } } _ = self.sender.take().unwrap().send(Ok(())); } } type CodeCacheReadyCallback = Box<dyn FnOnce(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>; pub(crate) struct CodeCacheInfo { data: Option<Cow<'static, [u8]>>, ready_callback: CodeCacheReadyCallback, } pub const BOM_CHAR: &[u8] = &[0xef, 0xbb, 0xbf]; /// Strips the byte order mark from the provided text if it exists. fn strip_bom(source_code: &[u8]) -> &[u8] { if source_code.starts_with(BOM_CHAR) { &source_code[BOM_CHAR.len()..] } else { source_code } } /// A `FuturesUnordered` paired with a `Cell<bool>` flag that tracks whether /// the collection has pending items. The flag avoids borrowing the `RefCell` /// just to check `is_empty()`. struct TrackedFutures<F> { futs: RefCell<FuturesUnordered<F>>, pending: Cell<bool>, } impl<F> Default for TrackedFutures<F> { fn default() -> Self { Self { futs: Default::default(), pending: Cell::new(false), } } } impl<F: Future + Unpin> TrackedFutures<F> { fn is_pending(&self) -> bool { self.pending.get() } fn push(&self, fut: F) { self.futs.borrow_mut().push(fut); self.pending.set(true); } /// Polls the inner `FuturesUnordered`. When the result is not /// `Ready(Some(_))` (i.e., no more ready items), the pending flag is /// synced from the collection's emptiness. fn poll_next_unpin(&self, cx: &mut Context) -> Poll<Option<F::Output>> { let poll = self.futs.borrow_mut().poll_next_unpin(cx); if !matches!(poll, Poll::Ready(Some(_))) { self.pending.set(!self.futs.borrow().is_empty()); } poll } fn clear(&self) { self.futs.borrow_mut().clear(); self.pending.set(false); } } /// A `Vec<T>` paired with a `Cell<bool>` flag that tracks whether the /// collection has pending items. struct TrackedVec<T> { vec: RefCell<Vec<T>>, pending: Cell<bool>, } impl<T> Default for TrackedVec<T> { fn default() -> Self { Self { vec: RefCell::new(Vec::new()), pending: Cell::new(false), } } } impl<T> TrackedVec<T> { fn is_pending(&self) -> bool { self.pending.get() } fn push(&self, item: T) { self.vec.borrow_mut().push(item); self.pending.set(true); } fn take(&self) -> Vec<T> { let v = std::mem::take(self.vec.borrow_mut().deref_mut()); self.pending.set(false); v } fn set(&self, items: Vec<T>) { self.pending.set(!items.is_empty()); *self.vec.borrow_mut() = items; } fn borrow(&self) -> std::cell::Ref<'_, Vec<T>> { self.vec.borrow() } fn clear(&self) { self.vec.borrow_mut().clear(); self.pending.set(false); } } struct DynImportModEvaluate { load_id: ModuleLoadId, module_id: ModuleId, promise: v8::Global<v8::Promise>, } #[derive(Debug, Clone)] struct DynImportState { resolver: v8::Global<v8::PromiseResolver>, cped: v8::Global<v8::Value>, phase: ModuleImportPhase, } /// A collection of JS modules. pub(crate) struct ModuleMap { // Handling of futures for loading module sources // TODO(mmastrac): we should not be swapping this loader out pub(crate) loader: RefCell<Rc<dyn ModuleLoader>>, pub(crate) source_mapper: Rc<RefCell<SourceMapper>>, exception_state: Rc<ExceptionState>, dynamic_import_map: RefCell<HashMap<ModuleLoadId, DynImportState>>, preparing_dynamic_imports: TrackedFutures<Pin<Box<PrepareLoadFuture>>>, pending_dynamic_imports: TrackedFutures<StreamFuture<RecursiveModuleLoad>>, pending_dyn_mod_evaluations: TrackedVec<DynImportModEvaluate>, pending_tla_waiters: RefCell<HashMap<ModuleId, Vec<v8::Global<v8::PromiseResolver>>>>, pending_mod_evaluation: Cell<bool>, /// Set to `true` while inside `module.evaluate()` in `mod_evaluate`. /// Used to suppress microtask checkpoints in `lazy_load_es_module_with_code` /// during module evaluation, preventing premature draining of TLA-related microtasks. evaluating_top_level: Cell<bool>, code_cache_ready_futs: TrackedFutures<Pin<Box<CodeCacheReadyFuture>>>, module_waker: AtomicWaker, data: RefCell<ModuleMapData>, will_snapshot: bool, loading_internal_modules: Cell<bool>, /// A counter used to delay our dynamic import deadlock detection by one spin /// of the event loop. pub(crate) dyn_module_evaluate_idle_counter: Cell<u32>, /// Tracks module IDs currently being evaluated via `op_import_sync` to /// detect require() cycles that V8's module status alone cannot catch. pub(crate) import_sync_eval_stack: RefCell<Vec<ModuleId>>, } struct LoadingInternalModulesGuard<'a> { module_map: &'a ModuleMap, previous: bool, } impl<'a> LoadingInternalModulesGuard<'a> { fn new(module_map: &'a ModuleMap) -> Self { Self { module_map, previous: module_map.loading_internal_modules.replace(true), } } } impl Drop for LoadingInternalModulesGuard<'_> { fn drop(&mut self) { self.module_map.loading_internal_modules.set(self.previous); } } /// Outcome of compiling a module's source. pub(crate) enum NewModuleResult { Ready(ModuleId), } impl NewModuleResult { fn into_ready(self) -> ModuleId { match self { NewModuleResult::Ready(id) => id, } } } impl ModuleMap { /// There is a circular Rc reference between the module map and the futures, /// so when destroying the module map we need to clear the pending futures. pub(crate) fn destroy(&self) { self.dynamic_import_map.borrow_mut().clear(); self.preparing_dynamic_imports.clear(); self.pending_dynamic_imports.clear(); self.pending_dyn_mod_evaluations.clear(); self.pending_tla_waiters.borrow_mut().clear(); self.code_cache_ready_futs.clear(); std::mem::take(&mut *self.data.borrow_mut()); } pub(crate) fn next_load_id(&self) -> i32 { // TODO(mmastrac): move recursive module loading into here so we can avoid making this pub let mut data = self.data.borrow_mut(); let id = data.next_load_id; data.next_load_id += 1; id + 1 } #[cfg(debug_assertions)] pub(crate) fn check_all_modules_evaluated( &self, scope: &mut v8::PinScope, ) -> Result<(), CoreError> { let mut not_evaluated = vec![]; let data = self.data.borrow(); for (handle, i) in data.handles_inverted.iter() { let module = v8::Local::new(scope, handle); match module.get_status() { v8::ModuleStatus::Errored => { return Err( CoreErrorKind::Js(JsError::from_v8_exception( scope, module.get_exception(), )) .into_box(), ); } v8::ModuleStatus::Evaluated => {} _ => { not_evaluated.push(data.info[*i].name.as_str().to_string()); } } } if !not_evaluated.is_empty() { return Err(CoreErrorKind::NonEvaluatedModules(not_evaluated).into_box()); } Ok(()) } pub(crate) fn new( loader: Rc<dyn ModuleLoader>, source_mapper: Rc<RefCell<SourceMapper>>, exception_state: Rc<ExceptionState>, will_snapshot: bool, ) -> Self { Self { will_snapshot, loader: loader.into(), source_mapper, exception_state, dyn_module_evaluate_idle_counter: Default::default(), dynamic_import_map: Default::default(), preparing_dynamic_imports: Default::default(), pending_dynamic_imports: Default::default(), pending_dyn_mod_evaluations: Default::default(), pending_tla_waiters: Default::default(), pending_mod_evaluation: Default::default(), evaluating_top_level: Default::default(), code_cache_ready_futs: Default::default(), module_waker: Default::default(), data: Default::default(), loading_internal_modules: Default::default(), import_sync_eval_stack: Default::default(), } } pub(crate) fn set_loading_internal_modules(&self, value: bool) { self.loading_internal_modules.set(value); } pub(crate) fn update_with_snapshotted_data( &self, scope: &mut v8::PinScope, data_store: &mut SnapshotLoadDataStore, data: ModuleMapSnapshotData, ) { self .data .borrow_mut() .update_with_snapshotted_data(scope, data_store, data); } /// Get module id, following all aliases in case of module specifier /// that had been redirected. pub(crate) fn get_id( &self, name: &str, requested_module_type: impl AsRef<RequestedModuleType>, ) -> Option<ModuleId> { self.data.borrow().get_id(name, requested_module_type) } /// Register an additional `(name, requested_module_type) -> module_id` /// mapping for an already-registered module. See /// `ModuleMapData::register_under_type`. pub(crate) fn register_under_type( &self, name: FastString, requested_module_type: &RequestedModuleType, module_id: ModuleId, ) { self.data.borrow_mut().register_under_type( name, requested_module_type, module_id, ); } pub(crate) fn is_main_module(&self, global: &v8::Global<v8::Module>) -> bool { self.data.borrow().is_main_module(global) } pub(crate) fn is_main_module_id(&self, id: ModuleId) -> bool { self.data.borrow().main_module_id == Some(id) } pub(crate) fn get_name_by_module( &self, global: &v8::Global<v8::Module>, ) -> Option<String> { self.data.borrow().get_name_by_module(global) } pub(crate) fn get_name_by_id(&self, id: ModuleId) -> Option<String> { self.data.borrow().get_name_by_id(id) } pub(crate) fn get_type_by_module( &self, global: &v8::Global<v8::Module>, ) -> Option<ModuleType> { self.data.borrow().get_type_by_module(global) } pub(crate) fn get_handle( &self, id: ModuleId, ) -> Option<v8::Global<v8::Module>> { self.data.borrow().get_handle(id) } pub(crate) fn serialize_for_snapshotting( &self, data_store: &mut SnapshotStoreDataStore, ) -> ModuleMapSnapshotData { let data = std::mem::take(&mut *self.data.borrow_mut()); data.serialize_for_snapshotting(data_store) } #[cfg(test)] pub fn is_alias( &self, name: &str, requested_module_type: impl AsRef<RequestedModuleType>, ) -> bool { self.data.borrow().is_alias(name, requested_module_type) } pub(crate) fn get_data(&self) -> &RefCell<ModuleMapData> { &self.data } #[cfg(test)] pub fn assert_module_map(&self, modules: &Vec<super::ModuleInfo>) { self.data.borrow().assert_module_map(modules); } #[cfg(all(test, not(miri)))] pub(crate) fn new_module( &self, scope: &mut v8::PinScope, main: bool, dynamic: bool, module_source: ModuleSource, ) -> Result<ModuleId, ModuleError> { Ok( self .new_module_with_pending(scope, main, dynamic, module_source)? .into_ready(), ) } pub(crate) fn new_module_with_pending( &self, scope: &mut v8::PinScope, main: bool, dynamic: bool, module_source: ModuleSource, ) -> Result<NewModuleResult, ModuleError> { let ModuleSource { code, module_type, module_url_found, module_url_specified, code_cache, } = module_source; // Register the module in the module map unless it's already there. If the // specified URL and the "true" URL are different, register the alias. let module_url_found = if let Some(module_url_found) = module_url_found { let (module_url_found1, module_url_found2) = module_url_found.into_cheap_copy(); self.data.borrow_mut().alias( module_url_specified, &module_type.clone().into(), module_url_found1, ); module_url_found2 } else { module_url_specified }; // TODO(bartlomieju): I have a hunch that this is wrong - write a test // that tries to "confuse" the type system, by first requesting a module // with type `RequestedModuleType::Other("foo".into)``, and then the loader // actually returns `ModuleType::Other("bar".into())`. See if it leads to // unexpected result in how `ModuleMap` is structured and verify how // querying the module map works (`ModuleMap::get_by_id`, `ModuleMap::get_by_name`). let requested_module_type = RequestedModuleType::from(module_type.clone()); let maybe_module_id = self.get_id(&module_url_found, requested_module_type); if let Some(module_id) = maybe_module_id { return Ok(NewModuleResult::Ready(module_id)); } let module_id = match module_type { ModuleType::JavaScript => { let code = ModuleSource::get_string_source(code); let (code_cache_info, module_url_found) = if let Some(code_cache) = code_cache { let (module_url_found1, module_url_found2) = module_url_found.into_cheap_copy(); let loader = self.loader.borrow().clone(); ( Some(CodeCacheInfo { data: code_cache.data, ready_callback: Box::new(move |cache| { let specifier = ModuleSpecifier::parse(module_url_found1.as_str()).unwrap(); loader.code_cache_ready(specifier, code_cache.hash, cache) }), }), module_url_found2, ) } else { (None, module_url_found) }; self .new_module_from_js_source_with_pending( scope, main, ModuleType::JavaScript, module_url_found, code, dynamic, code_cache_info, )? .into_ready() } ModuleType::Wasm => { self.new_wasm_module(scope, module_url_found, code, dynamic)? } ModuleType::Json => self.new_json_module( scope, module_url_found, ModuleSource::get_string_source(code), )?, ModuleType::Text => self.new_text_module( scope, module_url_found, ModuleSource::get_string_source(code), )?, ModuleType::Bytes => { let ModuleSourceCode::Bytes(code) = code else { return Err(ModuleError::Concrete( ModuleConcreteError::BytesNotBytes, )); }; self.new_bytes_module(scope, module_url_found, code)? } ModuleType::Other(module_type) => { let state = JsRuntime::state_from(scope); let custom_module_evaluation_cb = state.custom_module_evaluation_cb.as_ref(); let Some(custom_evaluation_cb) = custom_module_evaluation_cb else { return Err(ModuleError::Concrete( ModuleConcreteError::UnsupportedKind(module_type.to_string()), )); }; // TODO(bartlomieju): creating a global just to create a local from it // seems superfluous. However, changing `CustomModuleEvaluationCb` to have // a lifetime will have a viral effect and required `JsRuntimeOptions` // to have a callback as well as `JsRuntime`. let module_evaluation_kind = custom_evaluation_cb( scope, module_type.clone(), &module_url_found, code, ) .map_err(|e| ModuleError::Core(e.into()))?; match module_evaluation_kind { // Simple case, we just got a single value so we create a regular // synthetic module. CustomModuleEvaluationKind::Synthetic(value_global) => { let value = v8::Local::new(scope, value_global); let exports = vec![(ascii_str!("default"), value)]; self.new_synthetic_module( scope, module_url_found, ModuleType::Other(module_type.clone()), exports, ) } // Complex case - besides a synthetic module, we will create a new // module from JS code. CustomModuleEvaluationKind::ComputedAndSynthetic( computed_src, synthetic_value, synthetic_module_type, ) => { let (url1, url2) = module_url_found.into_cheap_copy(); let value = v8::Local::new(scope, synthetic_value); let exports = vec![(ascii_str!("default"), value)]; let _synthetic_mod_id = self.new_synthetic_module( scope, url1, synthetic_module_type, exports, ); let (code_cache_info, url2) = if let Some(code_cache) = code_cache { let (url1, url2) = url2.into_cheap_copy(); let loader = self.loader.borrow().clone(); ( Some(CodeCacheInfo { data: code_cache.data, ready_callback: Box::new(move |cache| { let specifier = ModuleSpecifier::parse(url1.as_str()).unwrap(); loader.code_cache_ready(specifier, code_cache.hash, cache) }), }), url2, ) } else { (None, url2) }; self .new_module_from_js_source_with_pending( scope, main, ModuleType::Other(module_type.clone()), url2, computed_src, dynamic, code_cache_info, )? .into_ready() } } } }; Ok(NewModuleResult::Ready(module_id)) } /// Creates a synthetic module whose exports mirror the own string-keyed /// properties of `exports_obj`, plus a `default` export pointing at /// `exports_obj` itself. Matches the shape of Node's /// `BuiltinModule.getESMFacade` so a CJS-style polyfill module can be /// imported as ESM without a hand-written wrapper. /// /// Property values are read once at creation time — synthetic exports /// are static snapshots, not live references back to the object. pub fn new_synthetic_module_from_exports_object<'s, 'i>( &self, scope: &mut v8::PinScope<'s, 'i>, name: impl IntoModuleName, exports_obj: v8::Local<'s, v8::Object>, ) -> ModuleId { let name = name.into_module_name(); let name_str = name.v8_string(scope).unwrap(); // Enumerate own string-keyed properties of the exports object. let property_names = exports_obj .get_own_property_names( scope, v8::GetPropertyNamesArgsBuilder::new() .mode(v8::KeyCollectionMode::OwnOnly) .property_filter(v8::PropertyFilter::SKIP_SYMBOLS) .key_conversion(v8::KeyConversionMode::ConvertToString) .build(), ) .unwrap(); let len = property_names.length(); let mut export_names: Vec<v8::Local<v8::String>> = Vec::with_capacity(len as usize + 1); let mut export_values: Vec<v8::Local<v8::Value>> = Vec::with_capacity(len as usize + 1); // If the IIFE returns `{ default: <ns>, ...named }`, treat the inner // `default` as the ESM default export. This mirrors the manual // `export default mod.default` pattern used by the old `*_esm.ts` // wrappers and Node's behavior for builtins whose `module.exports` // includes a `default` property. Otherwise fall back to the entire // exports object as the default (matches `module.exports = { ... }` // shape). let mut default_value: v8::Local<v8::Value> = exports_obj.into(); for i in 0..len { let key_val = property_names.get_index(scope, i).unwrap(); let key_str = key_val.to_string(scope).unwrap(); let value = exports_obj.get(scope, key_val).unwrap(); if key_str.to_rust_string_lossy(scope) == "default" { default_value = value; continue; } export_names.push(key_str); export_values.push(value); } let default_str = v8::String::new(scope, "default").unwrap(); export_names.push(default_str); export_values.push(default_value); let module = v8::Module::create_synthetic_module( scope, name_str, &export_names, synthetic_module_evaluation_steps, ); let handle = v8::Global::<v8::Module>::new(scope, module); let mut exports_global = Vec::with_capacity(export_names.len()); for i in 0..export_names.len() { exports_global.push(( v8::Global::new(scope, export_names[i]), v8::Global::new(scope, export_values[i]), )); } self .data .borrow_mut() .synthetic_module_exports_store .insert(handle.clone(), exports_global); let id = self.data.borrow_mut().create_module_info( name, ModuleType::JavaScript, handle, false, vec![], ); // Synthetic modules have no imports so their instantation must never fail. self.instantiate_module(scope, id).unwrap(); // Eagerly evaluate so the `synthetic_module_evaluation_steps` callback // fires now (which sets the exports from the staged store) instead of // at first read. Important during snapshot creation: V8 needs the // module in `Evaluated` state with its exports populated before the // snapshot is serialized; otherwise consumers that look up the // module's namespace at snapshot-finalize time hit // "GetModuleNamespace must be used on an instantiated module" or get // unbound exports. Evaluation is synchronous for synthetic modules. { let handle = self.get_handle(id).unwrap(); let local = v8::Local::new(scope, handle); let _ = local.evaluate(scope); } id } /// Creates a "synthetic module", that contains only a single, "default" export. /// /// The module gets instantiated and its ID is returned. pub fn new_synthetic_module<'s, 'i>( &self, scope: &mut v8::PinScope<'s, 'i>, name: impl IntoModuleName, module_type: ModuleType, exports: Vec<(FastStaticString, v8::Local<'s, v8::Value>)>, ) -> ModuleId { let name = name.into_module_name(); let name_str = name.v8_string(scope).unwrap(); let export_names = exports .iter() .map(|(name, _)| name.v8_string(scope).unwrap()) .collect::<Vec<_>>(); let module = v8::Module::create_synthetic_module( scope, name_str, &export_names, synthetic_module_evaluation_steps, ); let handle = v8::Global::<v8::Module>::new(scope, module); let mut exports_global = Vec::with_capacity(exports.len()); for i in 0..exports.len() { let export_name = export_names[i]; let (_, export_value) = exports[i]; exports_global.push(( v8::Global::new(scope, export_name), v8::Global::new(scope, export_value), )); } self .data .borrow_mut() .synthetic_module_exports_store .insert(handle.clone(), exports_global); let id = self.data.borrow_mut().create_module_info( name, module_type, handle, false, vec![], ); // Synthetic modules have no imports so their instantation must never fail. self.instantiate_module(scope, id).unwrap(); id } // TODO(bartlomieju): remove this method or rename it to `new_js_module`. /// Create and compile an ES module. pub(crate) fn new_es_module( &self, scope: &mut v8::PinScope, main: bool, name: ModuleName, source: ModuleCodeString, is_dynamic_import: bool, code_cache_info: Option<CodeCacheInfo>, ) -> Result<ModuleId, ModuleError> { self.new_module_from_js_source( scope, main, ModuleType::JavaScript, name, source, is_dynamic_import, code_cache_info, ) } /// Provided given JavaScript source code, compile and create a module of given /// type. /// /// Passed type doesn't have to be [`ModuleType::JavaScript`]! This method /// can be used to create "shim" modules, that execute some JS and act as a /// proxy to the actual underlying module (eg. you might create a "shim" for /// Wasm module). /// /// Imports in the executed code are parsed (along their import attributes) /// and attached to associated [`ModuleInfo`]. /// /// Returns an ID of newly created module. /// /// Sync call sites can use this directly. #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")] pub(crate) fn new_module_from_js_source( &self, scope: &mut v8::PinScope, main: bool, module_type: ModuleType, name: ModuleName, source: ModuleCodeString, is_dynamic_import: bool, code_cache_info: Option<CodeCacheInfo>, ) -> Result<ModuleId, ModuleError> { Ok( self .new_module_from_js_source_with_pending( scope, main, module_type, name, source, is_dynamic_import, code_cache_info, )? .into_ready(), ) } /// Same as [`new_module_from_js_source`] but returns [`NewModuleResult`]. #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")] pub(crate) fn new_module_from_js_source_with_pending( &self, scope: &mut v8::PinScope, main: bool, module_type: ModuleType, name: ModuleName, source: ModuleCodeString, is_dynamic_import: bool, mut code_cache_info: Option<CodeCacheInfo>, ) -> Result<NewModuleResult, ModuleError> { if main { let data = self.data.borrow(); if let Some(main_module) = data.main_module_id { let main_name = self.data.borrow().get_name_by_id(main_module).unwrap(); return Err(ModuleError::Concrete( ModuleConcreteError::MainModuleAlreadyExists { main_module: main_name.to_string(), new_module: name.to_string(), }, )); } } let _loading_internal_modules_guard = is_internal_module_specifier(name.as_str()) .then(|| LoadingInternalModulesGuard::new(self)); let name_str = name.v8_string(scope).unwrap(); let source_str = source.v8_string(scope).unwrap(); let host_defined_options = self .loader .borrow() .get_host_defined_options(scope, name.as_str()); let origin = script_origin(scope, name_str, true, host_defined_options); v8::tc_scope!(let tc_scope, scope); let (maybe_module, try_store_code_cache) = code_cache_info .as_ref() .and_then(|code_cache_info| { code_cache_info.data.as_ref().map(|cache| { let mut source = v8::script_compiler::Source::new_with_cached_data( source_str, Some(&origin), v8::CachedData::new(cache), ); let maybe_module = v8::script_compiler::compile_module2( tc_scope, &mut source, v8::script_compiler::CompileOptions::ConsumeCodeCache, v8::script_compiler::NoCacheReason::NoReason, ); // Check if the provided code cache is rejected by V8. let rejected = match source.get_cached_data() { Some(cached_data) => cached_data.rejected(), _ => true, }; (maybe_module, rejected) }) }) .unwrap_or_else(|| { let mut source = v8::script_compiler::Source::new(source_str, Some(&origin)); ( v8::script_compiler::compile_module(tc_scope, &mut source), true, ) }); if tc_scope.has_caught() { assert!(maybe_module.is_none()); let exception = tc_scope.exception().unwrap(); let exception = v8::Global::new(tc_scope, exception); // TODO(bartlomieju): add a more concrete variant - like `ModuleError::CompileError`? return Err(ModuleError::Exception(exception)); } let module = maybe_module.unwrap(); // V8 does not support creating code caches while also snapshotting, // and it's not needed anyway, as the snapshot already contains it. if try_store_code_cache && !self.will_snapshot && let Some(code_cache_info) = code_cache_info.take() { let unbound_module_script = module.get_unbound_module_script(tc_scope); let code_cache = unbound_module_script.create_code_cache().ok_or_else(|| { ModuleError::Concrete( ModuleConcreteError::UnboundModuleScriptCodeCache, ) })?; let fut = async move { (code_cache_info.ready_callback)(&code_cache).await } .boxed_local(); self.code_cache_ready_futs.push(fut); } // Extract native source map URL from V8 let unbound_module_script = module.get_unbound_module_script(tc_scope); let source_mapping_url_value = unbound_module_script.get_source_mapping_url(tc_scope); if !source_mapping_url_value.is_undefined() && !source_mapping_url_value.is_null() { let mut source_mapping_url_buf: [std::mem::MaybeUninit<u8>; 1024] = [std::mem::MaybeUninit::uninit(); 1024]; let source_mapping_url: v8::Local<v8::String> = source_mapping_url_value.try_cast().unwrap(); let source_mapping_url = source_mapping_url .to_rust_cow_lossy(tc_scope, &mut source_mapping_url_buf); let module_name = name .try_clone() .unwrap_or_else(|| ModuleName::from(name.as_str().to_string())); if source_mapping_url.starts_with(DATA_PREFIX) { if let Ok(DecodedMap::Regular(sm)) = sourcemap::decode_data_url(&source_mapping_url) { self .source_mapper .borrow_mut() .add_source_map(module_name, sm); } } else { // Resolve external source map URL relative to the module URL let resolved_url = if let Ok(module_url) = ModuleSpecifier::parse(name.as_str()) { module_url .join(&source_mapping_url) .unwrap_or(module_url) .to_string() } else { source_mapping_url.into_owned() }; self .source_mapper .borrow_mut() .add_source_map_url(module_name, resolved_url); } } // TODO(bartlomieju): maybe move to a helper function? let module_requests = module.get_module_requests(); let requests_len = module_requests.length(); let mut requests = Vec::with_capacity(requests_len); for i in 0..module_requests.length() { let module_request = v8::Local::<v8::ModuleRequest>::try_from( module_requests.get(tc_scope, i).unwrap(), ) .unwrap(); let mut import_specifier_buf: [std::mem::MaybeUninit<u8>; 1024] = [std::mem::MaybeUninit::uninit(); 1024]; let import_specifier = module_request .get_specifier() .to_rust_cow_lossy(tc_scope, &mut import_specifier_buf); let import_attributes = module_request.get_import_attributes(); let attributes = parse_import_attributes( tc_scope, import_attributes, ImportAttributesKind::StaticImport, ); // FIXME(bartomieju): there are no stack frames if exception // is thrown here { let state = JsRuntime::state_from(tc_scope); if let Some(validate_import_attributes_cb) = &state.validate_import_attributes_cb { let location = module .source_offset_to_location(module_request.get_source_offset()); let context = ImportAttributesContext { referrer: name.as_str().to_string(), specifier: import_specifier.to_string(), // V8 reports 0-based line/column; report them 1-based. line_number: Some(location.get_line_number() as u32 + 1), column_number: Some(location.get_column_number() as u32 + 1), }; (validate_import_attributes_cb)(tc_scope, &attributes, &context); } } if tc_scope.has_caught() { let exception = tc_scope.exception().unwrap(); let exception = v8::Global::new(tc_scope, exception); return Err(ModuleError::Exception(exception)); } let resolve_kind = if is_dynamic_import { ResolutionKind::DynamicImport } else { ResolutionKind::Import }; let module_specifier = match self.resolve_with_scope( tc_scope, &import_specifier, name.as_ref(), resolve_kind, &attributes, ) { Ok(s) => s, Err(e) => { // Fall back to lazy ESM sources for bare internal specifiers (e.g. // `node:_http_common` from `node:_http_outgoing`) that the // user-facing loader doesn't know about. If the specifier matches // a registered lazy ESM entry, use it verbatim. if self.has_lazy_esm_source(&import_specifier) && let Ok(parsed) = ModuleSpecifier::parse(&import_specifier) { parsed } else { return Err(ModuleError::Core(e.into())); } } }; let requested_module_type = get_requested_module_type_from_attributes(&attributes); let referrer_source_offset = if let ModuleType::Wasm = module_type { // Wasm sources will have been rendered to synthetic JS modules, so any // `ModuleRequest::referrer:source_offset`s we get from v8 are not // applicable to user code. Disregard it. None } else { Some(module_request.get_source_offset()) }; if crate::modules::import_graph::is_enabled() { crate::modules::import_graph::record_esm_import( name.as_ref(), module_specifier.as_str(), ); } let request = ModuleRequest { reference: ModuleReference { specifier: module_specifier, requested_module_type, }, specifier_key: Some(import_specifier.into_owned()), referrer_source_offset, phase: match module_request.get_phase() { v8::ModuleImportPhase::kEvaluation => ModuleImportPhase::Evaluation, v8::ModuleImportPhase::kSource => ModuleImportPhase::Source, v8::ModuleImportPhase::kDefer => ModuleImportPhase::Defer, }, }; requests.push(request); } let handle = v8::Global::<v8::Module>::new(tc_scope, module); let id = self.data.borrow_mut().create_module_info( name, module_type, handle, main, requests, ); Ok(NewModuleResult::Ready(id)) } pub(crate) fn new_wasm_module_source( &self, scope: &mut v8::PinScope, module_reference: &ModuleReference, mut loaded_source: ModuleSource, ) -> Result<ModuleSource, ModuleError> { if let Some(module_url_found) = loaded_source.cheap_copy_module_url_found() { self.data.borrow_mut().alias( loaded_source.cheap_copy_module_url_specified(), &loaded_source.module_type.clone().into(), module_url_found, ); } let reference_key = ModuleSourceKey::from_reference(module_reference); if self.data.borrow().sources.contains_key(&reference_key) { return Ok(loaded_source); } let loaded_key = ModuleSourceKey::from_loaded_source(&mut loaded_source); if let Some(source) = self.data.borrow().sources.get(&loaded_key).cloned() { self.data.borrow_mut().sources.insert(reference_key, source); return Ok(loaded_source); } let ModuleSourceCode::Bytes(code) = &loaded_source.code else { return Err(ModuleError::Concrete(ModuleConcreteError::WasmNotBytes)); }; let Some(wasm_module) = v8::WasmModuleObject::compile(scope, code.as_bytes()) else { return Err( ModuleConcreteError::WasmCompile(loaded_key.name.to_string()).into(), ); }; let wasm_module_object: v8::Local<v8::Object> = wasm_module.into(); let source = v8::Global::new(scope, wasm_module_object); { let mut data = self.data.borrow_mut(); data.sources.insert(reference_key, source.clone()); data.sources.insert(loaded_key, source); } Ok(loaded_source) } pub(crate) fn new_wasm_module( &self, scope: &mut v8::PinScope, name: ModuleName, source: ModuleSourceCode, is_dynamic_import: bool, ) -> Result<ModuleId, ModuleError> { let bytes = source.as_bytes(); let wasm_module_analysis = WasmDeps::parse( bytes, wasm_dep_analyzer::ParseOptions { skip_types: true }, ) .map_err(ModuleConcreteError::WasmParse)?; let js_wasm_module_source = render_js_wasm_module(name.as_str(), wasm_module_analysis); self.new_module_from_js_source( scope, false, ModuleType::Wasm, name, js_wasm_module_source.into(), is_dynamic_import, None, ) } pub(crate) fn new_json_module( &self, scope: &mut v8::PinScope, name: impl IntoModuleName, code: impl IntoModuleCodeString, ) -> Result<ModuleId, ModuleError> { let name = name.into_module_name(); let code = code.into_module_code(); let source_str = v8::String::new_from_utf8( scope, strip_bom(code.as_bytes()), v8::NewStringType::Normal, ) .unwrap(); v8::tc_scope!(let tc_scope, scope); let parsed_json = match v8::json::parse(tc_scope, source_str) { Some(parsed_json) => parsed_json, None => { assert!(tc_scope.has_caught()); let exception = tc_scope.exception().unwrap(); let exception = v8::Global::new(tc_scope, exception); return Err(ModuleError::Exception(exception)); } }; let exports = vec![(ascii_str!("default"), parsed_json)]; Ok(self.new_synthetic_module(tc_scope, name, ModuleType::Json, exports)) } #[allow( clippy::unnecessary_wraps, reason = "consistent return type with other module constructors" )] pub(crate) fn new_text_module( &self, scope: &mut v8::PinScope, name: impl IntoModuleName, code: impl IntoModuleCodeString, ) -> Result<ModuleId, ModuleError> { let name = name.into_module_name(); let code = code.into_module_code(); // TODO(bartlomieju): would be much better if the string was ensured to not contain // BOM, then we could use a more efficient string type with `FastString::v8_string`. let source_str = v8::String::new_from_utf8( scope, strip_bom(code.as_bytes()), v8::NewStringType::Normal, ) .unwrap(); let source_str_local = v8::Local::new(scope, source_str); let source_value_local = v8::Local::<v8::Value>::from(source_str_local); let exports = vec![(ascii_str!("default"), source_value_local)]; Ok(self.new_synthetic_module(scope, name, ModuleType::Text, exports)) } #[allow( clippy::unnecessary_wraps, reason = "consistent return type with other module constructors" )] pub(crate) fn new_bytes_module( &self, scope: &mut v8::PinScope, name: impl IntoModuleName, code: ModuleCodeBytes, ) -> Result<ModuleId, ModuleError> { let name = name.into_module_name(); let (buf_len, backing_store) = match code { ModuleCodeBytes::Static(bytes) => ( bytes.len(), v8::ArrayBuffer::new_backing_store_from_vec(bytes.to_vec()), ), ModuleCodeBytes::Boxed(bytes) => ( bytes.len(), v8::ArrayBuffer::new_backing_store_from_boxed_slice(bytes), ), ModuleCodeBytes::Arc(bytes) => ( bytes.len(), v8::ArrayBuffer::new_backing_store_from_vec(bytes.to_vec()), ), }; let backing_store_shared = backing_store.make_shared(); let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared); let uint8_array = v8::Uint8Array::new(scope, ab, 0, buf_len).unwrap(); let value: v8::Local<v8::Value> = uint8_array.into(); let exports = vec![(ascii_str!("default"), value)]; Ok(self.new_synthetic_module(scope, name, ModuleType::Bytes, exports)) } pub(crate) fn instantiate_module<'s, 'i>( &self, scope: &mut v8::PinScope<'s, 'i>, id: ModuleId, ) -> Result<(), v8::Global<v8::Value>> { v8::tc_scope!(let tc_scope, scope); let module = self .get_handle(id) .map(|handle| v8::Local::new(tc_scope, handle)) .expect("ModuleInfo not found"); if module.get_status() == v8::ModuleStatus::Errored { return Err(v8::Global::new(tc_scope, module.get_exception())); } // FIXME: instantiate_module is called more than it should be, // especially for dynamic imports. As a hack, bail out if the // module status is already being instantiated. if module.get_status() != v8::ModuleStatus::Uninstantiated { return Ok(()); } let _loading_internal_modules_guard = self .data .borrow() .get_name_by_id(id) .filter(|name| is_internal_module_specifier(name)) .map(|_| LoadingInternalModulesGuard::new(self)); tc_scope.set_slot(self as *const _); let instantiate_result = module.instantiate_module2( tc_scope, Self::module_resolve_callback, Self::module_source_callback, ); tc_scope.remove_slot::<*const Self>(); if instantiate_result.is_none() { let exception = tc_scope.exception().unwrap(); return Err(v8::Global::new(tc_scope, exception)); } Ok(()) } /// Called by V8 during `JsRuntime::instantiate_module`. This is only used internally, so we use the Isolate's annex /// to propagate a &Self. fn module_resolve_callback<'s>( context: v8::Local<'s, v8::Context>, specifier: v8::Local<'s, v8::String>, import_attributes: v8::Local<'s, v8::FixedArray>, referrer: v8::Local<'s, v8::Module>, ) -> Option<v8::Local<'s, v8::Module>> { // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>` v8::callback_scope!(unsafe scope, context); let module_map = // SAFETY: We retrieve the pointer from the slot, having just set it a few stack frames up unsafe { scope.get_slot::<*const Self>().unwrap().as_ref().unwrap() }; let referrer_global = v8::Global::new(scope, referrer); let referrer_name = module_map .data .borrow() .get_name_by_module(&referrer_global) .expect("ModuleInfo not found"); let mut specifier_buf: [std::mem::MaybeUninit<u8>; 1024] = [std::mem::MaybeUninit::uninit(); 1024]; let specifier_str = specifier.to_rust_cow_lossy(scope, &mut specifier_buf); let attributes = parse_import_attributes( scope, import_attributes, ImportAttributesKind::StaticImport, ); let requested_module_type = get_requested_module_type_from_attributes(&attributes); let pre_resolved_specifier = { let module_map_data = module_map.data.borrow(); let referrer_info = module_map_data .get_info_by_module(&referrer_global) .expect("ModuleInfo not found"); referrer_info .requests .iter() .find(|r| { r.specifier_key .as_ref() .is_some_and(|s| s == &specifier_str) && r.reference.requested_module_type == requested_module_type }) .map(|r| r.reference.specifier.clone()) }; let maybe_module = module_map.resolve_callback( scope, &specifier_str, &referrer_name, attributes, pre_resolved_specifier, ); if let Some(module) = maybe_module { return Some(module); } crate::error::throw_js_error_class( scope, &JsErrorBox::type_error(format!( r#"Cannot resolve module "{specifier_str}" from "{referrer_name}""# )), ); None } fn module_source_callback<'s>( context: v8::Local<'s, v8::Context>, specifier: v8::Local<'s, v8::String>, import_attributes: v8::Local<'s, v8::FixedArray>, referrer: v8::Local<'s, v8::Module>, ) -> Option<v8::Local<'s, v8::Object>> { // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>` v8::callback_scope!(unsafe scope, context); let module_map = // SAFETY: We retrieve the pointer from the slot, having just set it a few stack frames up unsafe { scope.get_slot::<*const Self>().unwrap().as_ref().unwrap() }; let mut specifier_buf: [std::mem::MaybeUninit<u8>; 1024] = [std::mem::MaybeUninit::uninit(); 1024]; let specifier_str = specifier.to_rust_cow_lossy(scope, &mut specifier_buf); let referrer_global = v8::Global::new(scope, referrer); let attributes = parse_import_attributes( scope, import_attributes, ImportAttributesKind::StaticImport, ); let requested_module_type = get_requested_module_type_from_attributes(&attributes); let module_reference = { let module_map_data = module_map.data.borrow(); let referrer_info = module_map_data .get_info_by_module(&referrer_global) .expect("ModuleInfo not found"); let module_request = referrer_info .requests .iter() .find(|r| { r.specifier_key .as_ref() .is_some_and(|s| s == &specifier_str) && r.reference.requested_module_type == requested_module_type }) .expect("ModuleInfo::requests did not contain a matching specifier_key when getting source"); module_request.reference.clone() }; let key = ModuleSourceKey::from_reference(&module_reference); if let Some(entry) = module_map.data.borrow().sources.get(&key) { Some(v8::Local::new(scope, entry)) } else { let message = v8::String::new( scope, &format!(r#"Module source can not be imported for "{specifier_str}""#), ) .unwrap(); let exception = v8::Exception::reference_error(scope, message); scope.throw_exception(exception); None } } /// Resolve provided module. This function calls out to `loader.resolve`, /// but applies some additional checks that disallow resolving/importing /// certain modules (eg. `ext:` or `node:` modules). /// pub fn resolve( &self, specifier: &str, referrer: &str, kind: ResolutionKind, ) -> ModuleResolveResponse { if let Some(resolved_specifier) = self.maybe_resolve_internal_import(specifier, referrer) { return Ok(resolved_specifier); } let resolved_specifier = self.loader.borrow().resolve(specifier, referrer, kind)?; self.validate_ext_module_import(&resolved_specifier, referrer)?; Ok(resolved_specifier) } pub fn resolve_with_scope( &self, scope: &mut v8::PinScope, specifier: &str, referrer: &str, kind: ResolutionKind, import_attributes: &HashMap<String, String>, ) -> ModuleResolveResponse { if let Some(resolved_specifier) = self.maybe_resolve_internal_import(specifier, referrer) { return Ok(resolved_specifier); } let resolved_specifier = self.loader.borrow().resolve_with_scope( scope, specifier, referrer, kind, import_attributes, )?; self.validate_ext_module_import(&resolved_specifier, referrer)?; Ok(resolved_specifier) } /// Resolves an internal specifier imported by an internal module without /// consulting the loader. Returns `None` when this isn't such an import, in /// which case the loader decides. /// /// Internal modules that aren't baked into the snapshot get instantiated at /// runtime, at which point the installed loader is the embedder's /// user-facing one. In Deno that loader applies the user's import map, so an /// entry like `{ "ext:core/mod.js": "./mod.js" }` used to rewrite the imports /// of internal modules such as `ext:cli/40_test_common.js` or /// `node:_http_agent`, breaking instantiation. Internal specifiers are owned /// by the runtime and always resolve to themselves. /// /// This trusts the referrer only as far as `validate_ext_module_import` /// does — a `node:`-looking referrer that user code made up (e.g. via /// `node:vm`'s `filename` option) isn't enough on its own. fn maybe_resolve_internal_import( &self, specifier: &str, referrer: &str, ) -> Option<ModuleSpecifier> { if !self.is_internal_referrer(referrer) { return None; } let specifier = ModuleSpecifier::parse(specifier).ok()?; is_internal_scheme(specifier.scheme()).then_some(specifier) } fn validate_ext_module_import( &self, resolved_specifier: &ModuleSpecifier, referrer: &str, ) -> Result<(), JsErrorBox> { if resolved_specifier.scheme() != "ext" { return Ok(()); } if (self.will_snapshot || self.loading_internal_modules.get()) && referrer == "." { return Ok(()); } if self.is_internal_referrer(referrer) { return Ok(()); } let referrer = if referrer.is_empty() { "(no referrer)" } else { referrer }; let msg = format!( "Importing ext: modules is only allowed from ext: and node: modules. Tried to import {} from {}", resolved_specifier, referrer ); Err(JsErrorBox::type_error(msg)) } fn is_internal_referrer(&self, referrer: &str) -> bool { if !is_internal_module_specifier(referrer) { return false; } self.will_snapshot || self.loading_internal_modules.get() } /// Called by `module_resolve_callback` during module instantiation. fn resolve_callback<'s, 'i>( &self, scope: &mut v8::PinScope<'s, 'i>, specifier: &str, referrer: &str, import_attributes: HashMap<String, String>, pre_resolved_specifier: Option<ModuleSpecifier>, ) -> Option<v8::Local<'s, v8::Module>> { // Synthetic ESM dispatch first, by raw specifier. The active loader // may not know about the spec (e.g. `LazyEsmModuleLoader` only // resolves `lazy_loaded_esm` entries), so checking before // `resolve_sync` ensures the synthetic dispatch wins over a loader // "cannot resolve" error. `node:foo` specifiers are their own // canonical form, so no further resolution is needed. if self.has_synthetic_esm_module(specifier) { if let Some(id) = self.get_id(specifier, &RequestedModuleType::None) && let Some(handle) = self.get_handle(id) { return Some(v8::Local::new(scope, handle)); } if let Some(module) = self.try_resolve_synthetic_esm(scope, specifier) { return Some(module); } } let module_type = get_requested_module_type_from_attributes(&import_attributes); let resolved_specifier = match pre_resolved_specifier { Some(specifier) => specifier, None => match self.resolve_with_scope( scope, specifier, referrer, ResolutionKind::Import, &import_attributes, ) { Ok(s) => s, Err(e) => { // Fall back to lazy ESM sources for bare internal specifiers like // `node:_http_common` that the runtime's user-facing loader // doesn't know how to resolve (only public `node:` modules go // through the normal path). The lazy_esm registry has them by // exact specifier, so if the specifier is a registered lazy ESM // entry, use it verbatim instead of erroring. if self.has_lazy_esm_source(specifier) && let Ok(parsed) = ModuleSpecifier::parse(specifier) { parsed } else { crate::error::throw_js_error_class(scope, &e); return None; } } }, }; if let Some(id) = self.get_id(resolved_specifier.as_str(), module_type) && let Some(handle) = self.get_handle(id) { return Some(v8::Local::new(scope, handle)); } // Synthetic ESM dispatch (post-resolve): in case the loader returned // a redirected/normalized form, also check here. Most callers hit // the pre-resolve branch above. if let Some(module) = self.try_resolve_synthetic_esm(scope, resolved_specifier.as_str()) { return Some(module); } // Fallback: check lazy-loaded ESM sources (modules embedded in the // binary but not included in the snapshot). let maybe_source = self.take_lazy_esm_source(resolved_specifier.as_str()); if let Some(source_code) = maybe_source { match self.new_es_module( scope, false, resolved_specifier.into(), source_code, false, None, ) { Ok(mod_id) => { if let Some(handle) = self.get_handle(mod_id) { return Some(v8::Local::new(scope, handle)); } } Err(e) => { let err = e.into_error(scope, false, true); crate::error::throw_js_error_class(scope, &err); return None; } } } None } pub(crate) fn get_requested_modules( &self, id: ModuleId, ) -> Option<Vec<ModuleRequest>> { // TODO(mmastrac): Remove cloning. We were originally cloning this at the call sites but that's no excuse. self.data.borrow().info.get(id).map(|i| i.requests.clone()) } // Initiate loading of a module graph imported using `import()`. #[allow(clippy::too_many_arguments, reason = "internal code")] pub(crate) fn load_dynamic_import( self: Rc<Self>, scope: &mut v8::PinScope, specifier: String, referrer: String, requested_module_type: RequestedModuleType, phase: ModuleImportPhase, resolver_handle: v8::Global<v8::PromiseResolver>, cped_handle: v8::Global<v8::Value>, ) -> bool { let resolve_response = self.resolve_with_scope( scope, &specifier, &referrer, ResolutionKind::DynamicImport, &HashMap::new(), ); // Fast path: if the module is already loaded, resolve the import // immediately without async work. if phase == ModuleImportPhase::Evaluation && let ref resolve_result = resolve_response && let Ok(module_specifier) = resolve_result && let Some(id) = self .data .borrow() .get_id(module_specifier.as_str(), &requested_module_type) { let module = self .data .borrow() .get_handle(id) .map(|handle| v8::Local::new(scope, handle)) .expect("Dyn import module info not found"); if module.get_status() == v8::ModuleStatus::Evaluated { // Check if this module has a pending TLA (top-level await) evaluation. let has_pending_tla = self .pending_dyn_mod_evaluations .borrow() .iter() .any(|pending| pending.module_id == id); // Queue this resolver to be resolved when the TLA completes. if has_pending_tla { self .pending_tla_waiters .borrow_mut() .entry(id) .or_default() .push(resolver_handle); return false; } // No pending TLA, safe to resolve immediately let resolver = resolver_handle.open(scope); let module_namespace = module.get_module_namespace(); resolver.resolve(scope, module_namespace).unwrap(); return false; } } // Fast path for lazy-loaded ESM: load synchronously and resolve // immediately, avoiding the async RecursiveModuleLoad path entirely. if phase == ModuleImportPhase::Evaluation && let ref resolve_result = resolve_response && let Ok(module_specifier) = resolve_result && self.has_lazy_esm_source(module_specifier.as_str()) { match self.lazy_load_esm_module(scope, module_specifier.as_str()) { Ok(module_ns) => { let resolver = resolver_handle.open(scope); let module_ns_local = v8::Local::new(scope, module_ns); resolver.resolve(scope, module_ns_local).unwrap(); return false; } Err(e) => { let exception = e.to_v8_error(scope); let exception_local = v8::Local::new(scope, exception); let resolver = resolver_handle.open(scope); resolver.reject(scope, exception_local).unwrap(); return false; } } } // Fast path for `synthetic_esm`-registered modules: build the // synthetic module synchronously and resolve immediately, same // pattern as the lazy ESM fast path above. if phase == ModuleImportPhase::Evaluation && let ref resolve_result = resolve_response && let Ok(module_specifier) = resolve_result && self.has_synthetic_esm_module(module_specifier.as_str()) && !self .loader .borrow() .should_load_synthetic_esm(module_specifier.as_str()) { match self .lazy_load_synthetic_esm_module(scope, module_specifier.as_str()) { Ok(module_ns) => { let resolver = resolver_handle.open(scope); let module_ns_local = v8::Local::new(scope, module_ns); resolver.resolve(scope, module_ns_local).unwrap(); return false; } Err(e) => { let exception = e.to_v8_error(scope); let exception_local = v8::Local::new(scope, exception); let resolver = resolver_handle.open(scope); resolver.reject(scope, exception_local).unwrap(); return false; } } } let load = RecursiveModuleLoad::new_dynamic_import( specifier, referrer, requested_module_type, phase, self.clone(), resolve_response, ); self.dynamic_import_map.borrow_mut().insert( load.id(), DynImportState { resolver: resolver_handle, cped: cped_handle, phase, }, ); let load_id = load.id(); let fut = async move { let mut load = load; (load_id, load.prepare().await.map(|()| load)) } .boxed_local(); self.preparing_dynamic_imports.push(fut); true } pub(crate) fn has_pending_dynamic_imports(&self) -> bool { self.preparing_dynamic_imports.is_pending() || self.pending_dynamic_imports.is_pending() } pub(crate) fn has_pending_module_evaluation(&self) -> bool { self.pending_mod_evaluation.get() } pub(crate) fn has_pending_dyn_module_evaluation(&self) -> bool { self.pending_dyn_mod_evaluations.is_pending() } /// See [`JsRuntime::mod_evaluate`]. pub fn mod_evaluate<'s, 'i>( self: &Rc<Self>, scope: &mut v8::PinScope<'s, 'i>, id: ModuleId, ) -> impl Future<Output = Result<(), CoreError>> + use<> { v8::tc_scope!(tc_scope, scope); let module = self .get_handle(id) .map(|handle| v8::Local::new(tc_scope, handle)) .expect("ModuleInfo not found"); let mut status = module.get_status(); // If the module is already evaluated, return early as there's nothing to do if status == v8::ModuleStatus::Evaluated { return Either::Left(futures::future::ready(Ok(()))); } assert_eq!( status, v8::ModuleStatus::Instantiated, "Module not instantiated: {} ({})", self.get_name_by_id(id).unwrap(), id, ); let (sender, receiver) = oneshot::channel::<Result<_, Box<JsError>>>(); let receiver = receiver.map(|res| { res .map(|r| r.map_err(|r| CoreErrorKind::Js(r).into_box())) .unwrap_or_else(|_| Err(CoreErrorKind::ExecutionTerminated.into_box())) }); self.evaluating_top_level.set(true); let Some(value) = module.evaluate(tc_scope) else { self.evaluating_top_level.set(false); if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { let undefined = v8::undefined(tc_scope).into(); _ = sender .send(exception_to_err_result(tc_scope, undefined, true, false)); } else { debug_assert_eq!(module.get_status(), v8::ModuleStatus::Errored); } return Either::Right(receiver); }; self.evaluating_top_level.set(false); self.pending_mod_evaluation.set(true); // Update status after evaluating. status = module.get_status(); if self.exception_state.has_dispatched_exception() { // This will be overridden in `exception_to_err_result()`. let exception = v8::undefined(tc_scope).into(); sender .send(exception_to_err_result(tc_scope, exception, true, false)) .expect("Failed to send module evaluation error."); } else { debug_assert!( status == v8::ModuleStatus::Evaluated || status == v8::ModuleStatus::Errored ); let promise = v8::Local::<v8::Promise>::try_from(value) .expect("Expected to get promise as module evaluation result"); // If this is a main module, claim the main module notification functions let (notify, module) = if self.is_main_module_id(id) { let module = Some(v8::Global::new(tc_scope, module)); ( std::mem::take(&mut self.data.borrow_mut().main_module_callbacks), module, ) } else { (vec![], None) }; // Create a ModEvaluate instance and stash it in an external let evaluation = v8::External::new( tc_scope, Box::into_raw(Box::new(ModEvaluate { module_map: self.clone(), sender: Some(sender), notify, module, })) as _, ); fn get_sender(arg: v8::Local<v8::Value>) -> ModEvaluate { let sender = v8::Local::<v8::External>::try_from(arg).unwrap(); *unsafe { Box::from_raw(sender.value() as _) } } let on_fulfilled = Function::builder( |scope: &mut v8::PinScope<'_, '_>, args: v8::FunctionCallbackArguments<'_>, _rv: v8::ReturnValue| { let mut sender = get_sender(args.data()); sender.module_map.pending_mod_evaluation.set(false); sender.module_map.module_waker.wake(); sender.notify(scope); }, ) .data(evaluation.into()) .build(tc_scope); let on_rejected = Function::builder( |scope: &mut v8::PinScope<'_, '_>, args: v8::FunctionCallbackArguments<'_>, _rv: v8::ReturnValue| { let mut sender = get_sender(args.data()); sender.module_map.pending_mod_evaluation.set(false); sender.module_map.module_waker.wake(); _ = sender.sender.take().unwrap().send(Ok(())); scope.throw_exception(args.get(0)); }, ) .data(evaluation.into()) .build(tc_scope); // V8 GC roots all promises, so we don't need to worry about it after this // then2 will return None if the runtime is shutting down if on_fulfilled.is_none() || on_rejected.is_none() || promise .then2(tc_scope, on_fulfilled.unwrap(), on_rejected.unwrap()) .is_none() { // There are two reasons we could be here: // 1. The runtime is shutting down, and JS ops are disabled with termination exceptions. // 2. User code has tampered with the runtime globals in some way that prevents us from // attaching `on_fulfilled`/`on_rejected` to `promise`. // In these cases we still need to report something back, so synthesize the result from the // promise. // Unset pending mod evaluation as the handlers will never run. See debug_assert below. self.pending_mod_evaluation.set(false); let mut sender = get_sender(evaluation.into()); match promise.state() { PromiseState::Fulfilled => { if let Some(exception) = tc_scope.exception() { _ = sender.sender.take().unwrap().send(exception_to_err_result( tc_scope, exception, true, false, )); } else { // Module loaded OK sender.notify(tc_scope); } } PromiseState::Rejected => { // Module was rejected let err = promise.result(tc_scope); let err = JsError::from_v8_exception(tc_scope, err); _ = sender.sender.take().unwrap().send(Err(err)); } PromiseState::Pending => { // User code shouldn't be able to both cause the runtime to fail and leave the promise as // pending because the only way to adopt a pending promise is to use `await` and // `await` won't work if you've broken the runtime in such a way that `promise::then` // didn't work. debug_assert!(tc_scope.is_execution_terminating()); // Module pending, just drop the sender at this point -- we can't do anything with a shut-down runtime. drop(sender); } } } // Under Explicit microtask policy, run the module-evaluation // checkpoint here. This matches Node's ESM ordering: Promise and // queueMicrotask jobs queued during top-level module evaluation run // before process.nextTick callbacks queued in the same evaluation. // // For async module graphs (with TLA), this checkpoint is also critical // for draining V8-internal TLA resume microtasks. If skipped, the // evaluation promise may never resolve because V8's internal async // module evaluation state machine relies on these microtasks being // processed. tc_scope.perform_microtask_checkpoint(); } Either::Right(receiver) } /// Helper function that allows to evaluate a module and ensure it's fully /// evaluated without the need to poll the event loop. /// /// This is useful for evaluating internal modules that can't use Top-Level Await. pub(crate) fn mod_evaluate_sync( self: &Rc<Self>, scope: &mut v8::PinScope, id: ModuleId, ) -> Result<(), CoreError> { v8::tc_scope!(let tc_scope, scope); let module = self .get_handle(id) .map(|handle| v8::Local::new(tc_scope, handle)) .expect("ModuleInfo not found"); let status = module.get_status(); // If the module is already evaluated, return early as there's nothing to do if status == v8::ModuleStatus::Evaluated { return Ok(()); } assert_eq!( status, v8::ModuleStatus::Instantiated, "Module not instantiated: {} ({})", self.get_name_by_id(id).unwrap(), id, ); if module.is_graph_async() { return Err(CoreErrorKind::TLA.into_box()); } let Some(value) = module.evaluate(tc_scope) else { let exception = tc_scope.exception().unwrap(); return Err( CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, exception)) .into_box(), ); }; // Under Explicit microtask policy, V8 won't drain microtasks after // module.evaluate(). We must do it ourselves so that the module // evaluation promise resolves for synchronous modules. // // However, skip the checkpoint when we are inside a top-level // `module.evaluate()` call (i.e. `evaluating_top_level` is set), e.g. // when a CJS `require()` of an ES module fires while V8 is evaluating // an async module graph. Draining microtasks at that point can resume // a suspended TLA dependency while its parent module is still in the // Evaluating state on the stack; V8 then cannot propagate the // completion to the parent and the graph's evaluation promise stays // Pending forever. The module evaluated here has a synchronous graph // (checked above), so its promise settles without a checkpoint. if !self.evaluating_top_level.get() { tc_scope.perform_microtask_checkpoint(); } if let Some(exception) = tc_scope.exception() { return Err( CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, exception)) .into_box(), ); } let status = module.get_status(); debug_assert!( status == v8::ModuleStatus::Evaluated || status == v8::ModuleStatus::Errored ); let promise = v8::Local::<v8::Promise>::try_from(value) .expect("Expected to get promise as module evaluation result"); promise.mark_as_handled(); match promise.state() { PromiseState::Fulfilled => Ok(()), PromiseState::Rejected => { let err = promise.result(tc_scope); let exception_state = JsRealm::exception_state_from_scope(tc_scope); // TODO: remove after crrev.com/c/7595271 exception_state.track_promise_rejection( tc_scope, promise, v8::PromiseRejectEvent::PromiseHandlerAddedAfterReject, None, ); Err( CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, err)) .into_box(), ) } PromiseState::Pending => { unreachable!() } } } fn dynamic_import_module_evaluate( &self, scope: &mut v8::PinScope, id: ModuleId, load_id: ModuleLoadId, state: DynImportState, ) -> Result<(), CoreError> { let module_handle = self.get_handle(id).expect("ModuleInfo not found"); let status = { let module = module_handle.open(scope); module.get_status() }; match status { v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {} _ => return Ok(()), } // IMPORTANT: Top-level-await is enabled, which means that return value // of module evaluation is a promise. // // This promise is internal, and not the same one that gets returned to // the user. We add handlers to wake the event loop when the promise resolves // (or rejects). The catch handler also serves to prevent an exception if the internal promise // rejects. That will instead happen for the other if not handled by the user. // // For more details see: // https://github.com/denoland/deno/issues/4908 // https://v8.dev/features/top-level-await#module-execution-order v8::tc_scope!(let tc_scope, scope); let cped = v8::Local::new(tc_scope, state.cped); tc_scope.set_continuation_preserved_embedder_data(cped); let module = v8::Local::new(tc_scope, &module_handle); // Set `evaluating_top_level` so that any nested `lazy_load_esm_module` // calls (triggered e.g. by CJS `require()` chains under // `npm:` packages) skip their post-evaluate `perform_microtask_checkpoint`. // Draining microtasks while V8 is inside `module.evaluate()` on an // async module graph leaves the evaluation promise permanently // Pending — V8 advances AsyncModuleExecutionFulfilled for the resumed // TLA dep but cannot then run `ExecuteModule` on the still-evaluating // parent. self.evaluating_top_level.set(true); let maybe_value = module.evaluate(tc_scope); self.evaluating_top_level.set(false); // Update status after evaluating. let status = module.get_status(); if let Some(value) = maybe_value { debug_assert!( status == v8::ModuleStatus::Evaluated || status == v8::ModuleStatus::Errored ); fn wake_module( scope: &mut v8::PinScope<'_, '_>, _args: v8::FunctionCallbackArguments<'_>, _rv: v8::ReturnValue, ) { let module_map = JsRealm::module_map_from(scope); module_map.module_waker.wake(); } let promise = v8::Local::<v8::Promise>::try_from(value) .expect("Expected to get promise as module evaluation result"); let wake_module_cb = Function::builder(wake_module).build(tc_scope); if let Some(wake_module_cb) = wake_module_cb { promise.then2(tc_scope, wake_module_cb, wake_module_cb); } else { // If the runtime is shutting down, we can't attach the handlers. // It doesn't really matter though, because they're just for waking the // event loop. } let dyn_import_mod_evaluate = DynImportModEvaluate { load_id, module_id: id, promise: v8::Global::new(tc_scope, promise), }; self .pending_dyn_mod_evaluations .push(dyn_import_mod_evaluate); } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { return Err(CoreErrorKind::EvaluateDynamicImportedModule.into_box()); } else { assert_eq!(status, v8::ModuleStatus::Errored); } Ok(()) } // Returns true if some dynamic import was resolved. fn evaluate_dyn_imports(&self, scope: &mut v8::PinScope) -> bool { if !self.pending_dyn_mod_evaluations.is_pending() { return false; } let pending = self.pending_dyn_mod_evaluations.take(); let mut resolved_any = false; let mut still_pending = vec![]; for eval in pending { let promise = eval.promise.open(scope); match promise.state() { v8::PromiseState::Pending => { still_pending.push(eval); } v8::PromiseState::Fulfilled => { resolved_any = true; self.dynamic_import_resolve(scope, eval.load_id, eval.module_id); self.resolve_tla_waiters(scope, eval.module_id); } v8::PromiseState::Rejected => { resolved_any = true; let exception = v8::Global::new(scope, promise.result(scope)); self.dynamic_import_reject(scope, eval.load_id, exception.clone()); self.reject_tla_waiters(scope, eval.module_id, exception); } } } self.pending_dyn_mod_evaluations.set(still_pending); resolved_any } /// Resolve all waiters that are waiting for a module's TLA to complete. fn resolve_tla_waiters(&self, scope: &mut v8::PinScope, module_id: ModuleId) { let waiters = self.pending_tla_waiters.borrow_mut().remove(&module_id); if let Some(waiters) = waiters && let Some(module) = self .data .borrow() .get_handle(module_id) .map(|handle| v8::Local::new(scope, handle)) { let module_namespace = module.get_module_namespace(); for resolver_handle in waiters { let resolver = resolver_handle.open(scope); resolver.resolve(scope, module_namespace).unwrap(); } if !JsRealm::state_from_scope(scope).has_tick_scheduled() { scope.perform_microtask_checkpoint(); } } } /// Reject all waiters that are waiting for a module's TLA to complete. fn reject_tla_waiters( &self, scope: &mut v8::PinScope, module_id: ModuleId, exception: v8::Global<v8::Value>, ) { let waiters = self.pending_tla_waiters.borrow_mut().remove(&module_id); if let Some(waiters) = waiters { let exception = v8::Local::new(scope, exception); for resolver_handle in waiters { let resolver = resolver_handle.open(scope); resolver.reject(scope, exception).unwrap(); } if !JsRealm::state_from_scope(scope).has_tick_scheduled() { scope.perform_microtask_checkpoint(); } } } pub(crate) fn dynamic_import_reject( &self, scope: &mut v8::PinScope, id: ModuleLoadId, exception: v8::Global<v8::Value>, ) { let resolver_handle = self .dynamic_import_map .borrow_mut() .remove(&id) .expect("Invalid dynamic import id") .resolver; let resolver = resolver_handle.open(scope); let exception = v8::Local::new(scope, exception); resolver.reject(scope, exception).unwrap(); if !JsRealm::state_from_scope(scope).has_tick_scheduled() { scope.perform_microtask_checkpoint(); } } pub(crate) fn dynamic_import_resolve( &self, scope: &mut v8::PinScope, id: ModuleLoadId, mod_id: ModuleId, ) { let resolver_handle = self .dynamic_import_map .borrow_mut() .remove(&id) .expect("Invalid dynamic import id") .resolver; let resolver = resolver_handle.open(scope); let module = self .data .borrow() .get_handle(mod_id) .map(|handle| v8::Local::new(scope, handle)) .expect("Dyn import module info not found"); // Resolution success assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated); // IMPORTANT: No borrows to `ModuleMap` can be held at this point because // resolving the promise might initiate another `import()` which will // in turn call `bindings::host_import_module_dynamically_callback` which // will reach into `ModuleMap` from within the isolate. let module_namespace = module.get_module_namespace(); resolver.resolve(scope, module_namespace).unwrap(); self.dyn_module_evaluate_idle_counter.set(0); if !JsRealm::state_from_scope(scope).has_tick_scheduled() { scope.perform_microtask_checkpoint(); } } /// Drain all ready module loading work: preparing dynamic imports, /// loading dynamic imports, evaluating them, and flushing code cache /// futures. Loops until no more progress can be made. /// /// The waker from `cx` is registered so the event loop is woken when /// any module future makes progress. pub(crate) fn poll_progress( &self, cx: &mut Context, scope: &mut v8::PinScope, ) -> Result<(), CoreError> { let mut has_evaluated = true; // TODO(mmastrac): We register this waker unconditionally because we occasionally need to re-run // the event loop. Eventually we will want this method to correctly wake the waker on any forward // progress. self.module_waker.register(cx.waker()); // Run in a loop so that dynamic imports that only depend on another // dynamic import can be resolved in this event loop iteration. // // For example, a dynamically imported module like the following can be // immediately resolved after `dependency.ts` is fully evaluated, but it // wouldn't if not for this loop. // // await delay(1000); // await import("./dependency.ts"); // console.log("test") // // These dynamic import dependencies can be cross-realm: // // await delay(1000); // await new ShadowRealm().importValue("./dependency.js", "default"); // while has_evaluated { has_evaluated = false; loop { self.drain_prepare_dyn_imports(cx, scope); self.drain_dyn_imports(cx, scope)?; self.drain_code_cache_ready(cx); if self.evaluate_dyn_imports(scope) { has_evaluated = true; } else { break; } } } Ok(()) } /// Drain all ready preparing-dynamic-import futures, moving successful /// loads into `pending_dynamic_imports` and rejecting failures. fn drain_prepare_dyn_imports( &self, cx: &mut Context, scope: &mut v8::PinScope, ) { if !self.preparing_dynamic_imports.is_pending() { return; } while let Poll::Ready(Some((dyn_import_id, prepare_result))) = self.preparing_dynamic_imports.poll_next_unpin(cx) { match prepare_result { Ok(load) => { self .pending_dynamic_imports .push(StreamExt::into_future(load)); } Err(err) => { let exception = err.to_v8_error(scope); self.dynamic_import_reject(scope, dyn_import_id, exception); } } } } /// Drain all ready pending-dynamic-import streams, registering loaded /// modules and instantiating/evaluating completed imports. fn drain_dyn_imports( &self, cx: &mut Context, scope: &mut v8::PinScope, ) -> Result<(), CoreError> { if !self.pending_dynamic_imports.is_pending() { return Ok(()); } while let Poll::Ready(Some((maybe_result, mut load))) = self.pending_dynamic_imports.poll_next_unpin(cx) { let dyn_import_id = load.id(); match maybe_result { Some(Ok((request, info))) => { // A module (not necessarily the one dynamically imported) has been // fetched. Create and register it, and if successful, poll for the // next recursive-load event related to this dynamic import. match load.register_and_recurse(scope, &request, info) { Ok(RegisterOutcome::Done) => { // Keep importing until it's fully drained self .pending_dynamic_imports .push(StreamExt::into_future(load)); } Err(err) => { let exception = match err { ModuleError::Exception(e) => e, ModuleError::Core(e) => e.to_v8_error(scope), ModuleError::Concrete(e) => { CoreErrorKind::Module(e).to_v8_error(scope) } }; self.dynamic_import_reject(scope, dyn_import_id, exception); } } } Some(Err(err)) => { // A non-javascript error occurred; this could be due to an invalid // module specifier, or a problem with the source map, or a failure // to fetch the module source code. let exception = err.to_v8_error(scope); self.dynamic_import_reject(scope, dyn_import_id, exception); } None => { // Stream finished — the full module graph has been loaded. let state = self .dynamic_import_map .borrow() .get(&dyn_import_id) .unwrap() .clone(); match state.phase { ModuleImportPhase::Evaluation => { let module_id = load.root_module_id().expect("Root module should be loaded"); let result = self.instantiate_module(scope, module_id); if let Err(exception) = result { self.dynamic_import_reject(scope, dyn_import_id, exception); } self.dynamic_import_module_evaluate( scope, module_id, dyn_import_id, state, )?; } ModuleImportPhase::Defer => { // For defer phase imports, the module is instantiated but NOT // eagerly evaluated. We call evaluate_for_import_defer which // gathers and evaluates async transitive dependencies, then // resolve with a deferred namespace that triggers evaluation // on first property access. let module_id = load.root_module_id().expect("Root module should be loaded"); let result = self.instantiate_module(scope, module_id); if let Err(exception) = result { self.dynamic_import_reject(scope, dyn_import_id, exception); continue; } let module_handle = self.get_handle(module_id).expect("ModuleInfo not found"); v8::tc_scope!(let tc_scope, scope); let cped = v8::Local::new(tc_scope, state.cped.clone()); tc_scope.set_continuation_preserved_embedder_data(cped); let module = v8::Local::new(tc_scope, &module_handle); // Gather async transitive dependencies. Returns a promise // that resolves when all async deps are ready. let maybe_promise = module.evaluate_for_import_defer(tc_scope); let Some(promise_val) = maybe_promise else { let exception = tc_scope.exception().unwrap(); let exception = v8::Global::new(tc_scope, exception); self.dynamic_import_reject(tc_scope, dyn_import_id, exception); continue; }; // Get the deferred namespace — this triggers evaluation on // first property access. let module_namespace = module .get_module_namespace_with_phase(v8::ModuleImportPhase::kDefer); let promise = v8::Local::<v8::Promise>::try_from(promise_val) .expect("evaluate_for_import_defer should return a promise"); match promise.state() { v8::PromiseState::Fulfilled => { // All async deps are ready, resolve immediately. let resolver_handle = self .dynamic_import_map .borrow_mut() .remove(&dyn_import_id) .expect("Invalid dynamic import id") .resolver; let resolver = resolver_handle.open(tc_scope); resolver.resolve(tc_scope, module_namespace).unwrap(); tc_scope.perform_microtask_checkpoint(); } v8::PromiseState::Rejected => { let err = promise.result(tc_scope); let err = v8::Global::new(tc_scope, err); self.dynamic_import_reject(tc_scope, dyn_import_id, err); } v8::PromiseState::Pending => { // Async deps still loading. Store for later resolution. // The module_waker will wake us when the promise settles. fn wake_module( scope: &mut v8::PinScope<'_, '_>, _args: v8::FunctionCallbackArguments<'_>, _rv: v8::ReturnValue, ) { let module_map = JsRealm::module_map_from(scope); module_map.module_waker.wake(); } let wake_module_cb = v8::Function::builder(wake_module).build(tc_scope); if let Some(wake_module_cb) = wake_module_cb { promise.then2(tc_scope, wake_module_cb, wake_module_cb); } let dyn_import_mod_evaluate = DynImportModEvaluate { load_id: dyn_import_id, module_id, promise: v8::Global::new(tc_scope, promise), }; self .pending_dyn_mod_evaluations .push(dyn_import_mod_evaluate); } } } ModuleImportPhase::Source => { let module_reference = load.root_module_reference().expect( "Root module reference had to have been resolved to get here.", ); let key = ModuleSourceKey::from_reference(module_reference); let source = { let data = self.data.borrow(); let source = data.sources.get(&key).expect("Source had to have been inserted successfully, or recursion would error."); v8::Local::new(scope, source).into() }; let resolver = state.resolver.open(scope); resolver.resolve(scope, source).unwrap(); } } } } } Ok(()) } /// Drain all ready code-cache futures. fn drain_code_cache_ready(&self, cx: &mut Context) { if !self.code_cache_ready_futs.is_pending() { return; } while let Poll::Ready(Some(_)) = self.code_cache_ready_futs.poll_next_unpin(cx) {} } pub(crate) fn get_module<'s, 'i>( &self, scope: &v8::PinScope<'s, 'i>, module_id: ModuleId, ) -> Option<v8::Local<'s, v8::Module>> { self .data .borrow() .get_handle(module_id) .map(|g| v8::Local::new(scope, g)) } /// Returns the namespace object of a module. /// /// This is only available after module evaluation has completed. /// This function panics if module has not been instantiated. pub fn get_module_namespace( &self, scope: &mut v8::PinScope, module_id: ModuleId, ) -> Result<v8::Global<v8::Object>, CoreError> { let module_handle = self .data .borrow() .get_handle(module_id) .expect("ModuleInfo not found"); let module = module_handle.open(scope); if module.get_status() == v8::ModuleStatus::Errored { let exception = module.get_exception(); return exception_to_err_result(scope, exception, false, false) .map_err(|e| CoreErrorKind::Js(e).into_box()); } assert!(matches!( module.get_status(), v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated )); let module_namespace: v8::Local<v8::Object> = v8::Local::try_from(module.get_module_namespace())?; Ok(v8::Global::new(scope, module_namespace)) } fn get_stalled_top_level_await_message_for_module( &self, scope: &mut v8::PinScope, module_id: ModuleId, ) -> Vec<v8::Global<v8::Message>> { let data = self.data.borrow(); let module_handle = data.handles.get(module_id).unwrap(); let module = v8::Local::new(scope, module_handle); // v8::Module::GetStalledTopLevelAwaitMessage() must not be called on // a synthetic module. if module.is_synthetic_module() { return vec![]; } let stalled = module.get_stalled_top_level_await_message(scope); let mut messages = vec![]; for (_, message) in stalled { messages.push(v8::Global::new(scope, message)); } messages } pub(crate) fn find_stalled_top_level_await( &self, scope: &mut v8::PinScope, ) -> Vec<v8::Global<v8::Message>> { // First check if that's root module let root_module_id = self .data .borrow() .info .iter() .filter(|m| m.main) .map(|m| m.id) .next(); if let Some(root_module_id) = root_module_id { let messages = self .get_stalled_top_level_await_message_for_module(scope, root_module_id); if !messages.is_empty() { return messages; } } // It wasn't a top module, so iterate over all modules and try to find // any with stalled top level await for module_id in 0..self.data.borrow().handles.len() { let messages = self.get_stalled_top_level_await_message_for_module(scope, module_id); if !messages.is_empty() { return messages; } } vec![] } /// Load and evaluate an ES module provided the specifier and source code. /// /// The module should not have Top-Level Await (that is, it should be /// possible to evaluate it synchronously). /// /// It is caller's responsibility to ensure that not duplicate specifiers are /// passed to this method. pub(crate) fn lazy_load_es_module_with_code( &self, scope: &mut v8::PinScope, module_specifier: &str, source_code: ModuleCodeString, code_cache_info: Option<CodeCacheInfo>, ) -> Result<v8::Global<v8::Value>, CoreError> { let specifier = ModuleSpecifier::parse(module_specifier)?; let previous_loading_internal_modules = is_internal_scheme(specifier.scheme()) .then(|| self.loading_internal_modules.replace(true)); let result = self.lazy_load_es_module_with_code_inner( scope, specifier, source_code, code_cache_info, ); if let Some(previous) = previous_loading_internal_modules { self.loading_internal_modules.set(previous); } result } fn lazy_load_es_module_with_code_inner( &self, scope: &mut v8::PinScope, specifier: ModuleSpecifier, source_code: ModuleCodeString, code_cache_info: Option<CodeCacheInfo>, ) -> Result<v8::Global<v8::Value>, CoreError> { let mod_id = self .new_es_module( scope, false, specifier.into(), source_code, false, code_cache_info, ) .map_err(|e| e.into_error(scope, false, true))?; self.instantiate_module(scope, mod_id).map_err(|e| { let exception = v8::Local::new(scope, e); exception_to_err(scope, exception, false, true) })?; let module_handle = self.get_handle(mod_id).unwrap(); let module_local = v8::Local::<v8::Module>::new(scope, module_handle); let status = module_local.get_status(); assert_eq!(status, v8::ModuleStatus::Instantiated); let value = module_local.evaluate(scope).unwrap(); // Under Explicit microtask policy, drain microtasks so the module // evaluation promise resolves for synchronous modules. // // However, skip the checkpoint when we are inside a top-level // `module.evaluate()` call (i.e. `evaluating_top_level` is set). // Draining microtasks at this point can prematurely resolve // TLA-related microtasks (e.g. `await` resume jobs from eagerly- // resolved async ops), which prevents the module evaluation promise // from settling correctly later. if !self.evaluating_top_level.get() { scope.perform_microtask_checkpoint(); } let promise = v8::Local::<v8::Promise>::try_from(value).unwrap(); let result = promise.result(scope); if !result.is_undefined() { return Err( CoreErrorKind::Js(exception_to_err(scope, result, false, true)) .into_box(), ); } let status = module_local.get_status(); assert_eq!(status, v8::ModuleStatus::Evaluated); let mod_ns = module_local.get_module_namespace(); Ok(v8::Global::new(scope, mod_ns)) } /// Check if a lazy-loaded ESM module is known to exist for the given /// specifier. This checks the metadata set which survives snapshotting, /// not just the source code map. pub(crate) fn has_lazy_esm_source(&self, specifier: &str) -> bool { let data = self.data.borrow(); data.known_lazy_esm.borrow().contains(specifier) || residual_static_table_contains( data.residual_lazy_esm_sources, specifier, ) } /// Get a lazy-loaded ESM source by specifier. Returns a cheap clone of the /// source code if found, keeping it in the lazy sources map so concurrent /// loads of the same lazy specifier from independent `RecursiveModuleLoad` /// instances each get their own copy. The duplicate `ModuleSource`s are /// deduplicated by `new_module_with_pending`'s `get_id` check at register /// time. pub(crate) fn take_lazy_esm_source( &self, specifier: &str, ) -> Option<ModuleCodeString> { let data = self.data.borrow(); let mut sources = data.lazy_esm_sources.borrow_mut(); let entry = match sources.get_mut(specifier) { Some(entry) => entry, None => { let source = residual_source_from_static_table( data.residual_lazy_esm_sources, specifier, )?; data .consumed_lazy_specifiers .borrow_mut() .insert(specifier.to_string()); return Some(source); } }; // `into_cheap_copy` always returns two cheap handles to the same backing // storage (Arc or Static), so we can keep one in the map and return the // other. The Owned variant gets promoted to Arc in the process. let placeholder = ModuleCodeString::from_static(""); let owned = std::mem::replace(entry, placeholder); let (keep, give) = owned.into_cheap_copy(); *entry = keep; data .consumed_lazy_specifiers .borrow_mut() .insert(specifier.to_string()); Some(give) } pub(crate) fn add_lazy_loaded_esm_source( &self, specifier: ModuleName, code: ModuleCodeString, ) { let data = self.data.borrow_mut(); data .known_lazy_esm .borrow_mut() .insert(specifier.as_str().to_string()); assert!( data .lazy_esm_sources .borrow_mut() .insert(specifier, code) .is_none() ); } pub(crate) fn add_residual_lazy_loaded_sources( &self, lazy_js_sources: &'static [(&'static str, &'static str)], lazy_esm_sources: &'static [(&'static str, &'static str)], ) { debug_assert_residual_static_table_sorted(lazy_js_sources); debug_assert_residual_static_table_sorted(lazy_esm_sources); let mut data = self.data.borrow_mut(); data.residual_lazy_script_sources = lazy_js_sources; data.residual_lazy_esm_sources = lazy_esm_sources; } /// Lazy load and evaluate an ES module. Only modules that have been added /// during build time can be executed (the ones stored in /// `ModuleMapData::lazy_esm_sources`), not _any, random_ module. /// Sync load + evaluate path for a `synthetic_esm` module. Used by the /// dynamic-import fast path. Returns the module namespace. pub(crate) fn lazy_load_synthetic_esm_module( &self, scope: &mut v8::PinScope, module_specifier: &str, ) -> Result<v8::Global<v8::Value>, CoreError> { // Use existing module if already constructed. let cached_handle = { let data = self.data.borrow(); data .get_id(module_specifier, RequestedModuleType::None) .and_then(|id| data.get_handle(id)) }; if let Some(handle) = cached_handle { let handle_local = v8::Local::new(scope, handle); if handle_local.get_status() == v8::ModuleStatus::Instantiated { let value = handle_local.evaluate(scope).unwrap(); if !self.evaluating_top_level.get() { scope.perform_microtask_checkpoint(); } let promise = v8::Local::<v8::Promise>::try_from(value).unwrap(); let result = promise.result(scope); if !result.is_undefined() { return Err( CoreErrorKind::Js(exception_to_err(scope, result, false, true)) .into_box(), ); } } return Ok(v8::Global::new(scope, handle_local.get_module_namespace())); } let module_id = self.build_synthetic_esm_module(scope, module_specifier)?; let handle = self.get_handle(module_id).unwrap(); let handle_local = v8::Local::new(scope, handle); let value = handle_local.evaluate(scope).unwrap(); if !self.evaluating_top_level.get() { scope.perform_microtask_checkpoint(); } let promise = v8::Local::<v8::Promise>::try_from(value).unwrap(); let result = promise.result(scope); if !result.is_undefined() { return Err( CoreErrorKind::Js(exception_to_err(scope, result, false, true)) .into_box(), ); } Ok(v8::Global::new(scope, handle_local.get_module_namespace())) } pub(crate) fn lazy_load_esm_module( &self, scope: &mut v8::PinScope, module_specifier: &str, ) -> Result<v8::Global<v8::Value>, CoreError> { if !self.has_lazy_esm_source(module_specifier) { return Err( JsErrorBox::generic(format!( "Specifier \"{module_specifier}\" cannot be lazy-loaded as it was not included in the binary." )) .into(), ); } let (lazy_esm_sources, residual_lazy_esm_sources) = { let data = self.data.borrow(); ( data.lazy_esm_sources.clone(), data.residual_lazy_esm_sources, ) }; let loader = LazyEsmModuleLoader::new(lazy_esm_sources, residual_lazy_esm_sources); // Check if this module has already been loaded. We release the // `self.data` borrow before doing anything that could re-enter the // module map (notably `module.evaluate(scope)`, which can recursively // compile dependent modules and would otherwise panic with a // `RefCell already borrowed` at `new_module_from_js_source`). let cached_id_and_handle = { let module_map_data = self.data.borrow(); module_map_data .get_id(module_specifier, RequestedModuleType::None) .and_then(|id| module_map_data.get_handle(id).map(|h| (id, h))) }; if let Some((cached_id, handle)) = cached_id_and_handle { crate::modules::import_graph::record_lazy_esm_cached( scope, module_specifier, ); let handle_local = v8::Local::new(scope, handle); // The module may be present in the map but not yet instantiated -- // e.g. when this lazy load fires from inside a sibling module's // resolve-callback (a `synthetic_esm` backing script synchronously // calling `op_lazy_load_esm` for a peer that the surrounding // `RecursiveModuleLoad` has registered but not yet instantiated). // `get_module_namespace` requires Instantiated+, so drive the module // forward synchronously here. if handle_local.get_status() == v8::ModuleStatus::Uninstantiated { self.instantiate_module(scope, cached_id).map_err(|e| { let exception = v8::Local::new(scope, e); exception_to_err(scope, exception, false, true) })?; } // Returning the namespace before evaluation leaves `export const` // bindings in the temporal dead zone, so trigger evaluation here. if handle_local.get_status() == v8::ModuleStatus::Instantiated { let value = handle_local.evaluate(scope).unwrap(); if !self.evaluating_top_level.get() { scope.perform_microtask_checkpoint(); } let promise = v8::Local::<v8::Promise>::try_from(value).unwrap(); let result = promise.result(scope); if !result.is_undefined() { return Err( CoreErrorKind::Js(exception_to_err(scope, result, false, true)) .into_box(), ); } } let module = v8::Global::new(scope, handle_local.get_module_namespace()); return Ok(module); } // Cache miss: real load incoming. This is the call that pays the // parse/compile/evaluate cost at runtime. crate::modules::import_graph::record_lazy_esm(scope, module_specifier); let specifier = ModuleSpecifier::parse(module_specifier)?; let load_response = loader.load( &specifier, None, ModuleLoadOptions { is_dynamic_import: false, is_synchronous: false, requested_module_type: RequestedModuleType::None, }, ); let source = match load_response { ModuleLoadResponse::Sync(result) => result, ModuleLoadResponse::Async(fut) => futures::executor::block_on(fut), }?; // `LazyEsmModuleLoader` only knows the in-binary static sources and has no // DENO_DIR access, so `source.code_cache` is always `None` here. Read the // persisted V8 code cache from the REAL loader (Cli/EmbeddedModuleLoader) // instead — the same seam the residual `lazy_loaded_js` path uses. Without // this, residual ESM (node:process, node:module, the stream/net/tty // closure) re-pays parse+compile in every isolate on every cold start. let source_code = ModuleSource::get_string_source(source.code); // Hash key for the on-disk cache. `v8_string` borrows `&source_code`, so it // stays usable for the compile below. let v8_source = source_code.v8_string(scope).unwrap(); // Build a `CodeCacheInfo` whenever the loader returns `Some` — even when // `data` is `None` (cold run). The `Some`-with-`data: None` case is what // arms the write side so the first run stores; warm runs consume. let code_cache_info = self .loader .borrow() .get_code_cache(&specifier, &v8_source) .map(|info| { let loader = self.loader.borrow().clone(); CodeCacheInfo { data: info.data, // `specifier` is unused after this, so move it straight in. ready_callback: Box::new(move |cache| { loader.code_cache_ready(specifier, info.hash, cache) }), } }); self.lazy_load_es_module_with_code( scope, module_specifier, source_code, code_cache_info, ) } pub(crate) fn add_lazy_loaded_script_source( &self, specifier: ModuleName, code: ModuleCodeString, ) { let data = self.data.borrow_mut(); assert!( data .lazy_script_sources .borrow_mut() .insert(specifier, code) .is_none(), "Duplicate lazy script source" ); } /// Check if a specifier was registered as a `synthetic_esm` module. pub(crate) fn has_synthetic_esm_module(&self, specifier: &str) -> bool { let data = self.data.borrow(); let modules = data.synthetic_esm_modules.borrow(); let key: ModuleName = String::from(specifier).into(); modules.contains_key(&key) } /// Register a `(module_specifier -> backing_script_specifier)` mapping /// for the `synthetic_esm` dispatch. Called at extension init from each /// extension's `synthetic_esm_modules` list. pub(crate) fn add_synthetic_esm_module( &self, module_specifier: ModuleName, backing_specifier: ModuleName, ) { let data = self.data.borrow(); assert!( data .synthetic_esm_modules .borrow_mut() .insert(module_specifier, backing_specifier) .is_none(), "Duplicate synthetic_esm module mapping" ); } /// Build a synthetic module for a `synthetic_esm`-registered specifier /// by evaluating its backing script (cache hit on second+ call) and /// deriving exports from the returned IIFE object. Returns the new /// `ModuleId`, or an error if the specifier is not registered or the /// backing script did not return an object. pub(crate) fn build_synthetic_esm_module( &self, scope: &mut v8::PinScope, specifier: &str, ) -> Result<ModuleId, CoreError> { let backing_specifier = { let data = self.data.borrow(); let modules = data.synthetic_esm_modules.borrow(); let key: ModuleName = String::from(specifier).into(); modules.get(&key).map(|v| v.as_str().to_string()) } .ok_or_else(|| { CoreError::from(JsErrorBox::generic(format!( "Specifier {specifier} is not a synthetic_esm module" ))) })?; let exports_global = self.load_ext_script(scope, &backing_specifier)?; let exports_local = v8::Local::new(scope, exports_global); let exports_obj = v8::Local::<v8::Object>::try_from(exports_local) .map_err(|_| { CoreError::from(JsErrorBox::type_error(format!( "synthetic_esm backing script {backing_specifier} did not return an object" ))) })?; Ok(self.new_synthetic_module_from_exports_object( scope, String::from(specifier), exports_obj, )) } /// Convenience wrapper around `build_synthetic_esm_module` for the V8 /// `module_resolve_callback` path: returns the module's V8 handle if /// `specifier` is registered as a `synthetic_esm` target, or `None` if /// not. Throws into `scope` on evaluation failure. fn try_resolve_synthetic_esm<'s, 'i>( &self, scope: &mut v8::PinScope<'s, 'i>, specifier: &str, ) -> Option<v8::Local<'s, v8::Module>> { if !self.has_synthetic_esm_module(specifier) { return None; } match self.build_synthetic_esm_module(scope, specifier) { Ok(module_id) => { let handle = self.get_handle(module_id)?; Some(v8::Local::new(scope, handle)) } Err(e) => { crate::error::throw_js_error_class(scope, &e); None } } } /// Load and evaluate a script on demand. The evaluated result is /// cached in `loaded_script_results` so later callers (the JS /// `Deno.core.loadExtScript()` op, the `synthetic_esm` dispatch, etc.) /// share a single evaluation — the source is consumed on first eval, /// and re-evaluating polyfill IIFEs would clobber registered hooks and /// duplicate class identities. Circular dependencies are detected and /// cause an error. pub(crate) fn load_ext_script( &self, scope: &mut v8::PinScope, specifier: &str, ) -> Result<v8::Global<v8::Value>, CoreError> { crate::modules::import_graph::record_lazy_script(scope, specifier); let specifier_str = String::from(specifier); let data = self.data.borrow(); // Cache hit: return the previously evaluated result. { let specifier_key: ModuleName = specifier_str.clone().into(); if let Some(cached) = data.loaded_script_results.borrow().get(&specifier_key) { return Ok(cached.clone()); } } // Circular dependency detection. { let specifier_key: ModuleName = specifier_str.clone().into(); let loading = data.lazy_script_loading.borrow(); if loading.contains(&specifier_key) { return Err( JsErrorBox::generic(format!( "Circular dependency detected when loading script \"{specifier}\"" )) .into(), ); } } // Look up source. let source = { let specifier_key: ModuleName = specifier_str.clone().into(); let mut sources = data.lazy_script_sources.borrow_mut(); sources.remove(&specifier_key).or_else(|| { residual_source_from_static_table( data.residual_lazy_script_sources, specifier, ) }) }; let source = match source { Some(s) => s, None => { return Err( JsErrorBox::generic(format!( "Script \"{specifier}\" cannot be lazy-loaded as it was not included in the binary." )) .into(), ); } }; data .consumed_lazy_specifiers .borrow_mut() .insert(specifier_str.clone()); // Mark as loading for circular dep detection. data .lazy_script_loading .borrow_mut() .insert(specifier_str.clone().into()); // We need to drop `data` before executing the script, since the script // may call back into `load_ext_script` for its own dependencies. drop(data); // Each script's IIFE preamble destructures the snapshot-time // `__bootstrap` view (a frozen clone of `core.ops` captured before // `removeImportedOps()` runs). To avoid leaving `__bootstrap` on // `globalThis` (where user code or `Object.keys` would see it), we // compile the source as the body of a function with one named // parameter `__bootstrap` and invoke it with the captured value. The // script's `(function () { ... })()` is the function body's single // expression, so we wrap it with `return ( ... );` to surface the // IIFE's result as the function's return value. let name = v8::String::new(scope, specifier).unwrap(); let origin = v8::ScriptOrigin::new( scope, name.into(), 0, 0, false, -1, None, false, false, false, None, ); v8::tc_scope!(let tc_scope, scope); // The compile_function body is `"use strict"; return (<IIFE>);`. For // residual lazy scripts this wrapping is performed at build time // (`cli/snapshot/build.rs`), so `source` is a `&'static` external string we // can hand to V8 without an owned heap copy (avoiding a per-script source // string in the V8 heap). Sources that arrive unwrapped (e.g. consumed // during snapshot creation, before that path is build-time wrapped) are // wrapped here at runtime via `wrap_lazy_ext_script`. let v8_source = if AsRef::<str>::as_ref(&source) .starts_with("\"use strict\"; return (") { // Build-time wrapped residual: hand V8 the `&'static` external string // directly so the source stays off the V8 heap (file-backed/clean). source.v8_string(tc_scope).unwrap() } else { // Unwrapped (e.g. consumed during snapshot creation): wrap at runtime. let wrapped_source = wrap_lazy_ext_script(AsRef::<str>::as_ref(&source)); v8::String::new(tc_scope, &wrapped_source).unwrap() }; let bootstrap_param = v8::String::new(tc_scope, "__bootstrap").unwrap(); // Persist a V8 code cache for this residual `lazy_loaded_js` module through // the same on-disk (DENO_DIR) cache user scripts use, keyed by specifier + // source hash. Residuals ship source-only in the binary, so without this a // node-heavy program re-pays parse+compile of the whole node-polyfill // closure at runtime on every cold start. The first run compiles and // stores; warm runs consume and skip parse+compile. Producing and consuming // binary are identical, so the cache is always accepted. let cache_specifier = crate::ModuleSpecifier::parse(&specifier_str).ok(); let code_cache_info = cache_specifier .as_ref() .and_then(|spec| self.loader.borrow().get_code_cache(spec, &v8_source)); let (mut compile_source, compile_options) = match code_cache_info.as_ref().and_then(|i| i.data.as_ref()) { Some(data) => ( v8::script_compiler::Source::new_with_cached_data( v8_source, Some(&origin), v8::CachedData::new(data), ), v8::script_compiler::CompileOptions::ConsumeCodeCache, ), None => ( v8::script_compiler::Source::new(v8_source, Some(&origin)), v8::script_compiler::CompileOptions::NoCompileOptions, ), }; let function = match v8::script_compiler::compile_function( tc_scope, &mut compile_source, &[bootstrap_param], &[], compile_options, v8::script_compiler::NoCacheReason::NoReason, ) { Some(f) => f, None => { let exception = tc_scope.exception().unwrap(); let err = JsError::from_v8_exception(tc_scope, exception); self .data .borrow() .lazy_script_loading .borrow_mut() .remove(&ModuleName::from(specifier_str.clone())); return Err(CoreErrorKind::Js(err).into_box()); } }; // Store the freshly-compiled cache on the first run (cold), or if V8 // rejected the existing cache (e.g. the source changed). let rejected = compile_source .get_cached_data() .map(|d| d.rejected()) .unwrap_or(true); let had_data = code_cache_info .as_ref() .map(|i| i.data.is_some()) .unwrap_or(false); if (!had_data || rejected) && let (Some(spec), Some(info)) = (cache_specifier.as_ref(), code_cache_info.as_ref()) && let Some(cache) = function.create_code_cache() { let fut = self.loader.borrow().code_cache_ready( spec.clone(), info.hash, &cache[..], ); self.code_cache_ready_futs.push(fut); } let captured = self.data.borrow().captured_bootstrap.borrow().clone(); let bootstrap_arg = match &captured { Some(global) => v8::Local::new(tc_scope, global), None => v8::undefined(tc_scope).into(), }; let undefined: v8::Local<v8::Value> = v8::undefined(tc_scope).into(); let result = match function.call(tc_scope, undefined, &[bootstrap_arg]) { Some(value) => v8::Global::new(tc_scope, value), None => { assert!(tc_scope.has_caught()); let exception = tc_scope.exception().unwrap(); let err = JsError::from_v8_exception(tc_scope, exception); self .data .borrow() .lazy_script_loading .borrow_mut() .remove(&ModuleName::from(specifier_str.clone())); return Err(CoreErrorKind::Js(err).into_box()); } }; // Remove from loading set and cache the result. { let data = self.data.borrow(); data .lazy_script_loading .borrow_mut() .remove(&ModuleName::from(specifier_str.clone())); data .loaded_script_results .borrow_mut() .insert(ModuleName::from(specifier_str), result.clone()); } Ok(result) } /// Stash the snapshot-time `__bootstrap` view registered from JS via /// `op_set_captured_bootstrap`. `load_ext_script` injects this value as /// the `__bootstrap` parameter when invoking each lazy script's compiled /// function — keeping the value off `globalThis` so user code never sees /// it. pub(crate) fn set_captured_bootstrap(&self, value: v8::Global<v8::Value>) { *self.data.borrow().captured_bootstrap.borrow_mut() = Some(value); } /// The snapshot-time `__bootstrap` view stashed by `set_captured_bootstrap`, /// if any. Used by the deferred fast-call upgrade to also update the cloned /// `core.ops` that residual ext modules read through. pub(crate) fn captured_bootstrap(&self) -> Option<v8::Global<v8::Value>> { self.data.borrow().captured_bootstrap.borrow().clone() } } /// Skip past a lazy-loaded script's leading prologue (line/block comments and /// directive prologue like `"use strict";`) and return a slice that starts at /// the IIFE's opening `(function`. Returns `None` if no such opener is found /// — callers fall back to using the original source unchanged. fn strip_script_prologue(source: &str) -> Option<&str> { let mut rest = source; loop { let trimmed = rest.trim_start(); if let Some(after) = trimmed.strip_prefix("//") { // Line comment — skip to end of line. let nl = after.find('\n').map(|i| i + 1).unwrap_or(after.len()); rest = &after[nl..]; continue; } if let Some(after) = trimmed.strip_prefix("/*") { // Block comment — skip to closing `*/`. let end = after.find("*/").map(|i| i + 2).unwrap_or(after.len()); rest = &after[end..]; continue; } // Directive prologue: `"use strict";` or `'use strict';` (or any other // string-literal directive). A directive is a string literal followed // by a `;`/newline at the statement boundary. if trimmed.starts_with('"') || trimmed.starts_with('\'') { let quote = trimmed.as_bytes()[0]; let after_quote = &trimmed[1..]; if let Some(close) = after_quote.find(quote as char) { let after_str = after_quote[close + 1..].trim_start(); if let Some(after_semi) = after_str.strip_prefix(';') { rest = after_semi; continue; } } } if trimmed.starts_with("(function") { return Some(trimmed); } return None; } } /// Wrap a lazy ext-script (`loadExtScript`) source into the `compile_function` /// body we evaluate at load time: `"use strict"; return (<IIFE>);`. /// /// The IIFE's completion value is the script's exports object (what the /// original `Script::run` produced). We strip the leading prologue (comments + /// any `"use strict";` directive — a bare directive is a statement and can't /// sit inside `return ( ... )`) and the trailing `;`/whitespace so the IIFE is /// the single operand of `return ( ... )`, and re-emit `"use strict";` at the /// head so the body still runs in strict mode. /// /// Performed at build time for residual lazy scripts so the stored source is a /// `&'static` string that compiles without an owned heap copy; also used as the /// runtime fallback for sources that arrive unwrapped. pub fn wrap_lazy_ext_script(source: &str) -> String { let expr_start = strip_script_prologue(source).unwrap_or(source); let trimmed = expr_start.trim_end_matches(|c: char| c.is_whitespace() || c == ';'); format!("\"use strict\"; return ({trimmed});") } // Clippy thinks the return value doesn't need to be an Option, it's unaware // of the mapping that MapFnFrom<F> does for ResolveModuleCallback. #[allow( clippy::unnecessary_wraps, reason = "required by MapFnFrom<F> for ResolveModuleCallback" )] pub(crate) fn synthetic_module_evaluation_steps<'s>( context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>, ) -> Option<v8::Local<'s, v8::Value>> { // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>` v8::callback_scope!(unsafe scope, context); v8::tc_scope!(tc_scope, scope); let module_map = JsRealm::module_map_from(tc_scope); let handle = v8::Global::<v8::Module>::new(tc_scope, module); let exports = module_map .data .borrow_mut() .synthetic_module_exports_store .remove(&handle) .unwrap(); for (export_name, export_value) in exports { let name = v8::Local::new(tc_scope, export_name); let value = v8::Local::new(tc_scope, export_value); // This should never fail assert!( module .set_synthetic_module_export(tc_scope, name, value) .unwrap() ); assert!(!tc_scope.has_caught()); } // Since Top-Level Await is active we need to return a promise. // This promise is resolved immediately. let resolver = v8::PromiseResolver::new(tc_scope).unwrap(); let undefined = v8::undefined(tc_scope); resolver.resolve(tc_scope, undefined.into()); Some(resolver.get_promise(tc_scope).into()) } pub fn script_origin<'s, 'i>( s: &mut v8::PinScope<'s, 'i>, resource_name: v8::Local<'s, v8::String>, is_module: bool, host_defined_options: Option<v8::Local<'s, v8::Data>>, ) -> v8::ScriptOrigin<'s> { v8::ScriptOrigin::new( s, resource_name.into(), 0, 0, false, 0, None, false, false, is_module, host_defined_options, ) } /// Helper injected into the synthetic module for `.wasm` files that have global /// exports. Per the Wasm ESM integration, a global export is unwrapped to its /// underlying JS value (e.g. an `i32` global exports the number directly) /// instead of being exposed as a `WebAssembly.Global` object, matching Node.js. /// Reading `.value` throws for `v128` globals, so we fall back to the /// `WebAssembly.Global` object in that case. The value is read once at /// instantiation (a snapshot), so a later mutation of a mutable global is not /// reflected in the export, which also matches Node. Wasm modules importing /// the global are not affected by the snapshot: they link against the /// original `WebAssembly.Global` via `import.meta.wasmInstances`. const WASM_GLOBAL_UNWRAP_HELPER: &str = "const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } };\n"; /// Whether a Wasm export is a global. The export kind is read directly from the /// export section, so this is reliable even though we parse the module with /// `skip_types: true` and never resolve the global's value type. fn is_wasm_global_export(export_type: &wasm_dep_analyzer::ExportType) -> bool { matches!(export_type, wasm_dep_analyzer::ExportType::Global(_)) } /// Whether a Wasm import is a global. Like [`is_wasm_global_export`], the import /// kind is read directly from the import section, independent of the global's /// value type. fn is_wasm_global_import(import_type: &wasm_dep_analyzer::ImportType) -> bool { matches!(import_type, wasm_dep_analyzer::ImportType::Global(_)) } fn render_js_wasm_module(specifier: &str, wasm_deps: WasmDeps) -> String { struct NamedImport { escaped_name: String, is_global: bool, } struct ImportInfo { key_escaped: String, named_imports: Vec<NamedImport>, has_global_import: bool, } fn aggregate_wasm_module_imports<'a>( imports: &'a [wasm_dep_analyzer::Import], ) -> IndexMap<&'a str, ImportInfo> { let mut imports_map = IndexMap::with_capacity(imports.len()); for import in imports { let entry = imports_map .entry(import.module) .or_insert_with(|| ImportInfo { key_escaped: import.module.escape_default().to_string(), named_imports: Vec::new(), has_global_import: false, }); let is_global = is_wasm_global_import(&import.import_type); entry.has_global_import |= is_global; entry.named_imports.push(NamedImport { escaped_name: import.name.escape_default().to_string(), is_global, }); } imports_map } let aggregated_imports = aggregate_wasm_module_imports(&wasm_deps.imports); let exports = wasm_deps .exports .iter() .map(|e| { let escaped_name = if e.name == "default" { Cow::Borrowed(e.name) } else { Cow::Owned(e.name.escape_default().to_string()) }; (escaped_name, is_wasm_global_export(&e.export_type)) }) .collect::<Vec<_>>(); let has_global_export = exports.iter().any(|(_, is_global)| *is_global); StringBuilder::build(|builder| { builder.append("import source wasmMod from \""); builder.append(specifier); builder.append("\";\n"); // A module with global exports registers its instance exports under its // own namespace in `import.meta.wasmInstances`, so that an importing Wasm // module can link against the original `WebAssembly.Global` objects. if has_global_export { builder.append("import * as selfNs from \""); builder.append(specifier); builder.append("\";\n"); } if !aggregated_imports.is_empty() { for (i, (_, import_info)) in aggregated_imports.iter().enumerate() { if import_info.has_global_import { builder.append("import * as import_ns_"); builder.append(i); builder.append(" from \""); builder.append(&import_info.key_escaped); builder.append("\";\n"); } builder.append("import { "); for (name_index, named_import) in import_info.named_imports.iter().enumerate() { if name_index > 0 { builder.append(", "); } builder.append('"'); builder.append(&named_import.escaped_name); builder.append("\" as import_"); builder.append(i); builder.append('_'); builder.append(name_index); } builder.append(" } from \""); builder.append(&import_info.key_escaped); builder.append("\";\n"); } // For global-typed imports, prefer the original `WebAssembly.Global` // from the dependency's instance when the dependency is itself a Wasm // module, so that mutable globals stay direct references between Wasm // modules. The JS binding only carries the unwrapped snapshot value. // // Limitation: for a circular Wasm<->Wasm mutable-global import the // dependency may not be evaluated yet when this `.get()` runs, so it // returns `undefined` and we fall back to the snapshot number, which // fails instantiation with a `LinkError`. Node.js has the same gap. for (i, (_, import_info)) in aggregated_imports.iter().enumerate() { if import_info.has_global_import { builder.append("const wasmExports_"); builder.append(i); builder.append(" = import.meta.wasmInstances.get(import_ns_"); builder.append(i); builder.append(");\n"); } } builder.append("const importsObject = {\n"); for (i, (_, import_info)) in aggregated_imports.iter().enumerate() { builder.append(" \""); builder.append(&import_info.key_escaped); builder.append("\": {\n"); for (name_index, named_import) in import_info.named_imports.iter().enumerate() { builder.append(" \""); builder.append(&named_import.escaped_name); builder.append("\": "); if named_import.is_global { builder.append("wasmExports_"); builder.append(i); builder.append(" === undefined ? import_"); builder.append(i); builder.append('_'); builder.append(name_index); builder.append(" : wasmExports_"); builder.append(i); builder.append("[\""); builder.append(&named_import.escaped_name); builder.append("\"]"); } else { builder.append("import_"); builder.append(i); builder.append('_'); builder.append(name_index); } builder.append(",\n"); } builder.append(" },\n"); } builder.append("};\n"); builder.append("const modInstance = new import.meta.WasmInstance(wasmMod, importsObject);\n"); } else { builder.append( "const modInstance = new import.meta.WasmInstance(wasmMod);\n" ); } if has_global_export { // The generated source assumes `import.meta.wasmInstances` is present // whenever a module uses globals. The map shares `import.meta.WasmInstance`'s // lifecycle: both are absent only when WebAssembly is unavailable or during // snapshotting, where the `new import.meta.WasmInstance(...)` call above // would already have thrown. So if we reach here, the map exists. builder.append( "import.meta.wasmInstances.set(selfNs, modInstance.exports);\n", ); builder.append(WASM_GLOBAL_UNWRAP_HELPER); } for (idx, (escaped_name, is_global)) in exports.iter().enumerate() { if escaped_name == "default" { builder.append("export default "); if *is_global { builder.append("unwrapWasmGlobal(modInstance.exports."); builder.append(escaped_name); builder.append(")"); } else { builder.append("modInstance.exports."); builder.append(escaped_name); } builder.append(";\n"); } else { builder.append("const export"); builder.append(idx); builder.append(" = "); if *is_global { builder.append("unwrapWasmGlobal(modInstance.exports[\""); builder.append(escaped_name); builder.append("\"])"); } else { builder.append("modInstance.exports[\""); builder.append(escaped_name); builder.append("\"]"); } builder.append(";\nexport { export"); builder.append(idx); builder.append(" as \""); builder.append(escaped_name); builder.append("\" };\n"); } } }).unwrap() } #[test] fn test_render_js_wasm_module() { let deps = WasmDeps { imports: vec![], exports: vec![], }; let rendered = render_js_wasm_module("./foo.wasm", deps); pretty_assertions::assert_eq!( rendered, r#"import source wasmMod from "./foo.wasm"; const modInstance = new import.meta.WasmInstance(wasmMod); "#, ); let deps = WasmDeps { imports: vec![ wasm_dep_analyzer::Import { name: "foo", module: "./import.js", import_type: wasm_dep_analyzer::ImportType::Tag( wasm_dep_analyzer::TagType { kind: 1, type_index: 1, }, ), }, wasm_dep_analyzer::Import { name: "bar", module: "./import.js", import_type: wasm_dep_analyzer::ImportType::Function(1), }, wasm_dep_analyzer::Import { name: "fizz", module: "./import.js", import_type: wasm_dep_analyzer::ImportType::Function(2), }, wasm_dep_analyzer::Import { name: "buzz", module: "./buzz.js", import_type: wasm_dep_analyzer::ImportType::Function(3), }, ], exports: vec![ wasm_dep_analyzer::Export { name: "export1", index: 0, export_type: wasm_dep_analyzer::ExportType::Function(Ok( wasm_dep_analyzer::FunctionSignature { params: vec![], returns: vec![], }, )), }, wasm_dep_analyzer::Export { name: "export2", index: 1, export_type: wasm_dep_analyzer::ExportType::Table, }, wasm_dep_analyzer::Export { name: "export3", index: 2, export_type: wasm_dep_analyzer::ExportType::Memory, }, wasm_dep_analyzer::Export { name: "export4", index: 3, export_type: wasm_dep_analyzer::ExportType::Global(Ok( wasm_dep_analyzer::GlobalType { value_type: wasm_dep_analyzer::ValueType::F32, mutability: false, }, )), }, wasm_dep_analyzer::Export { name: "export5", index: 4, export_type: wasm_dep_analyzer::ExportType::Tag, }, wasm_dep_analyzer::Export { name: "export6", index: 5, export_type: wasm_dep_analyzer::ExportType::Unknown, }, wasm_dep_analyzer::Export { name: "default", index: 6, export_type: wasm_dep_analyzer::ExportType::Function(Ok( wasm_dep_analyzer::FunctionSignature { params: vec![], returns: vec![], }, )), }, ], }; let rendered = render_js_wasm_module("./foo.wasm", deps); pretty_assertions::assert_eq!( rendered, r#"import source wasmMod from "./foo.wasm"; import * as selfNs from "./foo.wasm"; import { "foo" as import_0_0, "bar" as import_0_1, "fizz" as import_0_2 } from "./import.js"; import { "buzz" as import_1_0 } from "./buzz.js"; const importsObject = { "./import.js": { "foo": import_0_0, "bar": import_0_1, "fizz": import_0_2, }, "./buzz.js": { "buzz": import_1_0, }, }; const modInstance = new import.meta.WasmInstance(wasmMod, importsObject); import.meta.wasmInstances.set(selfNs, modInstance.exports); const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } }; const export0 = modInstance.exports["export1"]; export { export0 as "export1" }; const export1 = modInstance.exports["export2"]; export { export1 as "export2" }; const export2 = modInstance.exports["export3"]; export { export2 as "export3" }; const export3 = unwrapWasmGlobal(modInstance.exports["export4"]); export { export3 as "export4" }; const export4 = modInstance.exports["export5"]; export { export4 as "export5" }; const export5 = modInstance.exports["export6"]; export { export5 as "export6" }; export default modInstance.exports.default; "#, ); let deps = WasmDeps { imports: vec![wasm_dep_analyzer::Import { name: "\n", module: "\n", import_type: wasm_dep_analyzer::ImportType::Function(1), }], exports: vec![wasm_dep_analyzer::Export { name: "\n", index: 0, export_type: wasm_dep_analyzer::ExportType::Function(Ok( wasm_dep_analyzer::FunctionSignature { params: vec![], returns: vec![], }, )), }], }; let rendered = render_js_wasm_module("./bar.wasm", deps); pretty_assertions::assert_eq!( rendered, r#"import source wasmMod from "./bar.wasm"; import { "\n" as import_0_0 } from "\n"; const importsObject = { "\n": { "\n": import_0_0, }, }; const modInstance = new import.meta.WasmInstance(wasmMod, importsObject); const export0 = modInstance.exports["\n"]; export { export0 as "\n" }; "#, ); } #[test] fn test_render_js_wasm_module_global_unwrap() { fn global( value_type: wasm_dep_analyzer::ValueType, mutability: bool, ) -> wasm_dep_analyzer::ExportType { wasm_dep_analyzer::ExportType::Global(Ok(wasm_dep_analyzer::GlobalType { value_type, mutability, })) } let deps = WasmDeps { imports: vec![], exports: vec![ // immutable numeric global -> unwrapped to its value at runtime wasm_dep_analyzer::Export { name: "answer", index: 0, export_type: global(wasm_dep_analyzer::ValueType::I32, false), }, // mutable numeric global -> still unwrapped (snapshot at instantiation) wasm_dep_analyzer::Export { name: "counter", index: 1, export_type: global(wasm_dep_analyzer::ValueType::I64, true), }, // unresolved value type (e.g. v128 / reference type) is still a global // by kind, so it is wrapped; the helper falls back to the // WebAssembly.Global object if reading `.value` throws (v128). wasm_dep_analyzer::Export { name: "vec", index: 2, export_type: global(wasm_dep_analyzer::ValueType::Unknown, false), }, // global whose value type failed to parse -> still wrapped by kind wasm_dep_analyzer::Export { name: "broken", index: 3, export_type: wasm_dep_analyzer::ExportType::Global(Err( wasm_dep_analyzer::ParseError::UnresolvedExportType, )), }, // non-global export is left untouched wasm_dep_analyzer::Export { name: "fn_export", index: 4, export_type: wasm_dep_analyzer::ExportType::Function(Ok( wasm_dep_analyzer::FunctionSignature { params: vec![], returns: vec![], }, )), }, // default export that is a global -> unwrapped wasm_dep_analyzer::Export { name: "default", index: 5, export_type: global(wasm_dep_analyzer::ValueType::F64, false), }, ], }; let rendered = render_js_wasm_module("./globals.wasm", deps); pretty_assertions::assert_eq!( rendered, r#"import source wasmMod from "./globals.wasm"; import * as selfNs from "./globals.wasm"; const modInstance = new import.meta.WasmInstance(wasmMod); import.meta.wasmInstances.set(selfNs, modInstance.exports); const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } }; const export0 = unwrapWasmGlobal(modInstance.exports["answer"]); export { export0 as "answer" }; const export1 = unwrapWasmGlobal(modInstance.exports["counter"]); export { export1 as "counter" }; const export2 = unwrapWasmGlobal(modInstance.exports["vec"]); export { export2 as "vec" }; const export3 = unwrapWasmGlobal(modInstance.exports["broken"]); export { export3 as "broken" }; const export4 = modInstance.exports["fn_export"]; export { export4 as "fn_export" }; export default unwrapWasmGlobal(modInstance.exports.default); "#, ); } #[test] fn test_render_js_wasm_module_global_import() { let deps = WasmDeps { imports: vec![ // global-typed import -> linked against the original // `WebAssembly.Global` when the dependency is itself a Wasm module // (found in `import.meta.wasmInstances`), falling back to the JS // binding otherwise wasm_dep_analyzer::Import { name: "counter", module: "./dep.wasm", import_type: wasm_dep_analyzer::ImportType::Global( wasm_dep_analyzer::GlobalType { value_type: wasm_dep_analyzer::ValueType::I32, mutability: true, }, ), }, // non-global import from the same module is untouched wasm_dep_analyzer::Import { name: "bump", module: "./dep.wasm", import_type: wasm_dep_analyzer::ImportType::Function(0), }, // module with no global imports gets no namespace import or lookup wasm_dep_analyzer::Import { name: "log", module: "./util.js", import_type: wasm_dep_analyzer::ImportType::Function(1), }, ], exports: vec![wasm_dep_analyzer::Export { name: "read", index: 0, export_type: wasm_dep_analyzer::ExportType::Function(Ok( wasm_dep_analyzer::FunctionSignature { params: vec![], returns: vec![wasm_dep_analyzer::ValueType::I32], }, )), }], }; let rendered = render_js_wasm_module("./main.wasm", deps); pretty_assertions::assert_eq!( rendered, r#"import source wasmMod from "./main.wasm"; import * as import_ns_0 from "./dep.wasm"; import { "counter" as import_0_0, "bump" as import_0_1 } from "./dep.wasm"; import { "log" as import_1_0 } from "./util.js"; const wasmExports_0 = import.meta.wasmInstances.get(import_ns_0); const importsObject = { "./dep.wasm": { "counter": wasmExports_0 === undefined ? import_0_0 : wasmExports_0["counter"], "bump": import_0_1, }, "./util.js": { "log": import_1_0, }, }; const modInstance = new import.meta.WasmInstance(wasmMod, importsObject); const export0 = modInstance.exports["read"]; export { export0 as "read" }; "#, ); }