diff --git a/core/modules/map.rs b/core/modules/map.rs index 5bc6ad311..9c996e4da 100644 --- a/core/modules/map.rs +++ b/core/modules/map.rs @@ -38,6 +38,7 @@ use crate::source_map::SourceMapper; use capacity_builder::StringBuilder; use deno_error::JsErrorBox; use futures::StreamExt; +use futures::future::Either; use futures::future::FutureExt; use futures::stream::FuturesUnordered; use futures::stream::StreamFuture; @@ -1157,15 +1158,16 @@ impl ModuleMap { .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, - "{} {} ({})", - if status == v8::ModuleStatus::Evaluated { - "Module already evaluated. Perhaps you've re-provided a module or extension that was already included in the snapshot?" - } else { - "Module not instantiated" - }, + "Module not instantiated: {} ({})", self.get_name_by_id(id).unwrap(), id, ); @@ -1185,7 +1187,7 @@ impl ModuleMap { } else { debug_assert_eq!(module.get_status(), v8::ModuleStatus::Errored); } - return receiver; + return Either::Right(receiver); }; self.pending_mod_evaluation.set(true); @@ -1312,7 +1314,7 @@ impl ModuleMap { tc_scope.perform_microtask_checkpoint(); } - receiver + Either::Right(receiver) } /// Helper function that allows to evaluate a module and ensure it's fully @@ -1331,15 +1333,16 @@ impl ModuleMap { .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, - "{} {} ({})", - if status == v8::ModuleStatus::Evaluated { - "Module already evaluated. Perhaps you've re-provided a module or extension that was already included in the snapshot?" - } else { - "Module not instantiated" - }, + "Module not instantiated: {} ({})", self.get_name_by_id(id).unwrap(), id, ); diff --git a/core/modules/tests.rs b/core/modules/tests.rs index f6bbd4ac9..916a114f4 100644 --- a/core/modules/tests.rs +++ b/core/modules/tests.rs @@ -2088,3 +2088,92 @@ fn invalid_utf8_module() { FastString::from_static("// \u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}") ); } + +#[tokio::test] +async fn evaluate_already_evaluated_module() { + // This test verifies that calling mod_evaluate on an already-evaluated module + // doesn't panic, but instead returns Ok(()) immediately. This can happen when + // the same module is specified both as a preload module and as the main module. + + let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with( + Url::parse("file:///main.js").unwrap(), + ascii_str!( + "globalThis.executionCount = (globalThis.executionCount || 0) + 1;" + ), + ))); + + let mut runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader.clone()), + ..Default::default() + }); + + let spec = resolve_url("file:///main.js").unwrap(); + + // Load and evaluate the module for the first time + let mod_id = runtime.load_main_es_module(&spec).await.unwrap(); + let receiver = runtime.mod_evaluate(mod_id); + runtime.run_event_loop(Default::default()).await.unwrap(); + receiver.await.unwrap(); + + // Verify it executed once + runtime + .execute_script("check1", "if (globalThis.executionCount !== 1) throw new Error('Expected 1 execution')") + .unwrap(); + + // Try to evaluate the same module again - this should not panic + let receiver2 = runtime.mod_evaluate(mod_id); + runtime.run_event_loop(Default::default()).await.unwrap(); + receiver2.await.unwrap(); + + // Verify it still only executed once (module was not re-executed) + runtime + .execute_script("check2", "if (globalThis.executionCount !== 1) throw new Error('Expected still 1 execution')") + .unwrap(); +} + +#[tokio::test] +async fn evaluate_already_evaluated_module_sync() { + // This test verifies that calling mod_evaluate_sync on an already-evaluated module + // doesn't panic, but instead returns Ok(()) immediately. + + let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with( + Url::parse("file:///main.js").unwrap(), + ascii_str!( + "globalThis.syncExecutionCount = (globalThis.syncExecutionCount || 0) + 1;" + ), + ))); + + let mut runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader.clone()), + ..Default::default() + }); + + let spec = resolve_url("file:///main.js").unwrap(); + + // Load the module + let mod_id = runtime.load_main_es_module(&spec).await.unwrap(); + + // Evaluate synchronously using scope + { + let module_map = runtime.module_map(); + deno_core::scope!(scope, runtime); + module_map.mod_evaluate_sync(scope, mod_id).unwrap(); + } + + // Verify it executed once + runtime + .execute_script("check1", "if (globalThis.syncExecutionCount !== 1) throw new Error('Expected 1 execution')") + .unwrap(); + + // Try to evaluate the same module again synchronously - should not panic + { + let module_map = runtime.module_map(); + deno_core::scope!(scope, runtime); + module_map.mod_evaluate_sync(scope, mod_id).unwrap(); + } + + // Verify it still only executed once (module was not re-executed) + runtime + .execute_script("check2", "if (globalThis.syncExecutionCount !== 1) throw new Error('Expected still 1 execution')") + .unwrap(); +}