Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions core/modules/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
);
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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,
);
Expand Down
89 changes: 89 additions & 0 deletions core/modules/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading