1use std::cell::RefCell;
4use std::collections::{HashMap, HashSet};
5use std::ffi::c_void;
6use std::num::NonZeroI32;
7use std::time::Instant;
8
9static MOD_TRACE: std::sync::OnceLock<std::sync::Mutex<[u64; 5]>> = std::sync::OnceLock::new();
15
16fn mod_trace_enabled() -> bool {
17 std::env::var("AGENTOS_MODULE_TRACE").as_deref() == Ok("1")
18}
19
20fn record_mod(idx: usize, ns: u64) {
21 let m = MOD_TRACE.get_or_init(|| std::sync::Mutex::new([0u64; 5]));
22 let Ok(mut a) = m.lock() else {
23 return;
24 };
25 a[idx] = a[idx].wrapping_add(ns);
26 if idx == 4 {
27 a[0] += 1;
28 if a[0] % 25 == 0 {
29 if let Ok(path) = std::env::var("AGENTOS_MODULE_TRACE_FILE") {
30 let _ = std::fs::write(
31 &path,
32 format!(
33 "modules={} resolve_ms={} load_ms={} format_ms={} compile_ms={}\n",
34 a[0],
35 a[1] / 1_000_000,
36 a[2] / 1_000_000,
37 a[3] / 1_000_000,
38 a[4] / 1_000_000,
39 ),
40 );
41 }
42 }
43 }
44}
45
46use crate::bridge::{deserialize_v8_value, serialize_v8_value};
47use crate::host_call::BridgeCallContext;
48use crate::ipc::ExecutionError;
49#[cfg(test)]
50use crate::ipc::{OsConfig, ProcessConfig};
51
52pub struct BridgeCodeCache {
59 source_hash: u64,
61 cached_data: Vec<u8>,
63}
64
65impl BridgeCodeCache {
66 fn hash_source(source: &str) -> u64 {
68 let mut hash: u64 = 0xcbf29ce484222325;
69 for byte in source.as_bytes() {
70 hash ^= *byte as u64;
71 hash = hash.wrapping_mul(0x100000001b3);
72 }
73 hash
74 }
75}
76
77#[cfg(test)]
82pub fn inject_globals(
83 scope: &mut v8::HandleScope,
84 process_config: &ProcessConfig,
85 os_config: &OsConfig,
86) {
87 let context = scope.get_current_context();
88 let global = context.global(scope);
89 let pc_obj = build_process_config(scope, process_config);
91 pc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
92 let pc_key = v8::String::new(scope, "_processConfig").unwrap();
93 let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
94 global.define_own_property(scope, pc_key.into(), pc_obj.into(), attr);
95
96 let os_obj = build_os_config(scope, os_config);
98 os_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
99 let os_key = v8::String::new(scope, "_osConfig").unwrap();
100 let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
101 global.define_own_property(scope, os_key.into(), os_obj.into(), attr);
102
103 }
108
109pub fn install_high_resolution_time_global(scope: &mut v8::HandleScope, origin: *const Instant) {
110 let context = scope.get_current_context();
111 let global = context.global(scope);
112 let external = v8::External::new(scope, origin as *mut c_void);
113 let template = v8::FunctionTemplate::builder(high_resolution_time_callback)
114 .data(external.into())
115 .build(scope);
116 let Some(func) = template.get_function(scope) else {
117 return;
118 };
119 let key = v8::String::new(scope, "__secureExecHrNowUs").unwrap();
120 let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
121 global.define_own_property(scope, key.into(), func.into(), attr);
122}
123
124fn high_resolution_time_callback(
125 scope: &mut v8::HandleScope,
126 args: v8::FunctionCallbackArguments,
127 mut rv: v8::ReturnValue,
128) {
129 let external = match v8::Local::<v8::External>::try_from(args.data()) {
130 Ok(ext) => ext,
131 Err(_) => {
132 let msg = v8::String::new(scope, "internal error: missing hrtime origin").unwrap();
133 let exc = v8::Exception::error(scope, msg);
134 scope.throw_exception(exc);
135 return;
136 }
137 };
138 let origin = unsafe { &*(external.value() as *const Instant) };
141 let micros = origin.elapsed().as_secs_f64() * 1_000_000.0;
142 rv.set(v8::Number::new(scope, micros).into());
143}
144
145pub fn inject_globals_from_payload(
151 scope: &mut v8::HandleScope,
152 payload: &[u8],
153) -> Result<(), ExecutionError> {
154 let context = scope.get_current_context();
155 let global = context.global(scope);
156
157 let config_val = deserialize_v8_value(scope, payload)
159 .map_err(|err| invalid_globals_payload_error(format!("decode failed: {err}")))?;
160
161 if !config_val.is_object() {
162 return Err(invalid_globals_payload_error("payload is not an object"));
163 }
164 let config_obj = v8::Local::<v8::Object>::try_from(config_val)
165 .map_err(|_| invalid_globals_payload_error("payload is not an object"))?;
166 if !is_plain_config_object(scope, config_obj) {
167 return Err(invalid_globals_payload_error(
168 "payload is not a plain object",
169 ));
170 }
171
172 let (pc_val, pc_obj) = required_object_property(scope, config_obj, "processConfig")?;
175 let (oc_val, oc_obj) = required_object_property(scope, config_obj, "osConfig")?;
176
177 let (_env_val, env_obj) =
178 required_object_property_with_label(scope, pc_obj, "env", "processConfig.env")?;
179 freeze_config_object(scope, env_obj, "processConfig.env")?;
180 freeze_config_object(scope, pc_obj, "processConfig")?;
181 freeze_config_object(scope, oc_obj, "osConfig")?;
182 let global_key = v8::String::new(scope, "_processConfig").unwrap();
183 let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
184 global.define_own_property(scope, global_key.into(), pc_val, attr);
185
186 let global_key = v8::String::new(scope, "_osConfig").unwrap();
187 let attr = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
188 global.define_own_property(scope, global_key.into(), oc_val, attr);
189
190 Ok(())
191}
192
193fn required_object_property<'s>(
194 scope: &mut v8::HandleScope<'s>,
195 obj: v8::Local<'s, v8::Object>,
196 name: &str,
197) -> Result<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>), ExecutionError> {
198 required_object_property_with_label(scope, obj, name, name)
199}
200
201fn required_object_property_with_label<'s>(
202 scope: &mut v8::HandleScope<'s>,
203 obj: v8::Local<'s, v8::Object>,
204 name: &str,
205 error_label: &str,
206) -> Result<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>), ExecutionError> {
207 let key = v8::String::new(scope, name).unwrap();
208 let value = obj
209 .get(scope, key.into())
210 .filter(|value| !value.is_null_or_undefined())
211 .ok_or_else(|| invalid_globals_payload_error(format!("missing {error_label}")))?;
212 if !value.is_object() {
213 return Err(invalid_globals_payload_error(format!(
214 "{error_label} is not an object"
215 )));
216 }
217 let object = v8::Local::<v8::Object>::try_from(value)
218 .map_err(|_| invalid_globals_payload_error(format!("{error_label} is not an object")))?;
219 if !is_plain_config_object(scope, object) {
220 return Err(invalid_globals_payload_error(format!(
221 "{error_label} is not a plain object"
222 )));
223 }
224 Ok((value, object))
225}
226
227fn is_plain_config_object(scope: &mut v8::HandleScope, object: v8::Local<v8::Object>) -> bool {
228 let Some(prototype) = object.get_prototype(scope) else {
229 return false;
230 };
231 if prototype.is_null() {
232 return true;
233 }
234 if !prototype.is_object() {
235 return false;
236 }
237 let Ok(prototype_object) = v8::Local::<v8::Object>::try_from(prototype) else {
238 return false;
239 };
240 prototype_object
241 .get_prototype(scope)
242 .is_some_and(|parent| parent.is_null())
243}
244
245fn freeze_config_object(
246 scope: &mut v8::HandleScope,
247 object: v8::Local<v8::Object>,
248 label: &str,
249) -> Result<(), ExecutionError> {
250 match object.set_integrity_level(scope, v8::IntegrityLevel::Frozen) {
251 Some(true) => Ok(()),
252 Some(false) | None => Err(invalid_globals_payload_error(format!(
253 "failed to freeze {label}"
254 ))),
255 }
256}
257
258fn invalid_globals_payload_error(message: impl Into<String>) -> ExecutionError {
259 ExecutionError {
260 error_type: "Error".into(),
261 message: format!("invalid InjectGlobals payload: {}", message.into()),
262 stack: String::new(),
263 code: Some("ERR_INVALID_GLOBALS_PAYLOAD".into()),
264 }
265}
266
267fn run_bridge_cached(
275 scope: &mut v8::HandleScope,
276 bridge_code: &str,
277 cache: &mut Option<BridgeCodeCache>,
278) -> (i32, Option<ExecutionError>) {
279 let tc = &mut v8::TryCatch::new(scope);
280
281 let v8_source = match v8::String::new(tc, bridge_code) {
282 Some(s) => s,
283 None => {
284 return (
285 1,
286 Some(ExecutionError {
287 error_type: "Error".into(),
288 message: "bridge code string too large for V8".into(),
289 stack: String::new(),
290 code: None,
291 }),
292 );
293 }
294 };
295
296 let resource_name = v8::String::new(tc, "<bridge>").unwrap();
298 let origin = v8::ScriptOrigin::new(
299 tc,
300 resource_name.into(),
301 0,
302 0,
303 false,
304 -1,
305 None,
306 false,
307 false,
308 false,
309 None,
310 );
311
312 let source_hash = BridgeCodeCache::hash_source(bridge_code);
313
314 let cache_hit = cache.as_ref().is_some_and(|c| c.source_hash == source_hash);
316
317 let script = if cache_hit {
318 let cached_bytes = &cache.as_ref().unwrap().cached_data;
320 let cached_data = v8::script_compiler::CachedData::new(cached_bytes);
321 let mut source = v8::script_compiler::Source::new_with_cached_data(
322 v8_source,
323 Some(&origin),
324 cached_data,
325 );
326 let compiled = v8::script_compiler::compile(
327 tc,
328 &mut source,
329 v8::script_compiler::CompileOptions::ConsumeCodeCache,
330 v8::script_compiler::NoCacheReason::NoReason,
331 );
332 if source.get_cached_data().is_some_and(|cd| cd.rejected()) {
334 *cache = None;
335 }
336 compiled
337 } else {
338 let mut source = v8::script_compiler::Source::new(v8_source, Some(&origin));
340 let compiled = v8::script_compiler::compile(
341 tc,
342 &mut source,
343 v8::script_compiler::CompileOptions::NoCompileOptions,
344 v8::script_compiler::NoCacheReason::NoReason,
345 );
346 if let Some(ref script) = compiled {
348 let unbound = script.get_unbound_script(tc);
349 if let Some(code_cache) = unbound.create_code_cache() {
350 *cache = Some(BridgeCodeCache {
351 source_hash,
352 cached_data: code_cache.to_vec(),
353 });
354 }
355 }
356 compiled
357 };
358
359 let script = match script {
361 Some(s) => s,
362 None => {
363 return match tc.exception() {
364 Some(e) => {
365 let (c, err) = exception_to_result(tc, e);
366 (c, Some(err))
367 }
368 None => (1, None),
369 };
370 }
371 };
372
373 if script.run(tc).is_none() {
374 return match tc.exception() {
375 Some(e) => {
376 let (c, err) = exception_to_result(tc, e);
377 (c, Some(err))
378 }
379 None => (1, None),
380 };
381 }
382
383 (0, None)
384}
385
386#[cfg(not(test))]
389pub fn run_init_script(scope: &mut v8::HandleScope, code: &str) -> (i32, Option<ExecutionError>) {
390 if code.is_empty() {
391 return (0, None);
392 }
393 let tc = &mut v8::TryCatch::new(scope);
394 let source = match v8::String::new(tc, code) {
395 Some(s) => s,
396 None => {
397 return (
398 1,
399 Some(ExecutionError {
400 error_type: "Error".into(),
401 message: "init script string too large for V8".into(),
402 stack: String::new(),
403 code: None,
404 }),
405 );
406 }
407 };
408 let script = match v8::Script::compile(tc, source, None) {
409 Some(s) => s,
410 None => {
411 return match tc.exception() {
412 Some(e) => {
413 let (c, err) = exception_to_result(tc, e);
414 (c, Some(err))
415 }
416 None => (1, None),
417 };
418 }
419 };
420 if script.run(tc).is_none() {
421 return match tc.exception() {
422 Some(e) => {
423 let (c, err) = exception_to_result(tc, e);
424 (c, Some(err))
425 }
426 None => (1, None),
427 };
428 }
429 (0, None)
430}
431
432pub fn execute_script(
438 scope: &mut v8::HandleScope,
439 bridge_code: &str,
440 user_code: &str,
441 bridge_cache: &mut Option<BridgeCodeCache>,
442) -> (i32, Option<ExecutionError>) {
443 execute_script_with_options(scope, None, bridge_code, user_code, None, bridge_cache)
444}
445
446pub fn execute_script_with_options(
447 scope: &mut v8::HandleScope,
448 bridge_ctx: Option<&BridgeCallContext>,
449 bridge_code: &str,
450 user_code: &str,
451 file_path: Option<&str>,
452 bridge_cache: &mut Option<BridgeCodeCache>,
453) -> (i32, Option<ExecutionError>) {
454 if let Some(bridge_ctx) = bridge_ctx {
455 MODULE_RESOLVE_STATE.with(|cell| {
456 *cell.borrow_mut() = Some(ModuleResolveState {
457 bridge_ctx: bridge_ctx as *const BridgeCallContext,
458 module_names: HashMap::new(),
459 module_cache: HashMap::new(),
460 guest_reader: None,
461 });
462 });
463 }
464
465 if !bridge_code.is_empty() {
467 let (code, err) = run_bridge_cached(scope, bridge_code, bridge_cache);
468 if code != 0 {
469 if bridge_ctx.is_some() {
470 clear_module_state();
471 }
472 return (code, err);
473 }
474 }
475
476 {
478 let tc = &mut v8::TryCatch::new(scope);
479 let source = match v8::String::new(tc, user_code) {
480 Some(s) => s,
481 None => {
482 if bridge_ctx.is_some() {
483 clear_module_state();
484 }
485 return (
486 1,
487 Some(ExecutionError {
488 error_type: "Error".into(),
489 message: "user code string too large for V8".into(),
490 stack: String::new(),
491 code: None,
492 }),
493 );
494 }
495 };
496 let origin = file_path.and_then(|path| {
497 let resource = v8::String::new(tc, path)?;
498 Some(v8::ScriptOrigin::new(
499 tc,
500 resource.into(),
501 0,
502 0,
503 false,
504 -1,
505 None,
506 false,
507 false,
508 false,
509 None,
510 ))
511 });
512 let script = match v8::Script::compile(tc, source, origin.as_ref()) {
513 Some(s) => s,
514 None => {
515 if bridge_ctx.is_some() {
516 clear_module_state();
517 }
518 return match tc.exception() {
519 Some(e) => {
520 let (c, err) = exception_to_result(tc, e);
521 (c, Some(err))
522 }
523 None => (1, None),
524 };
525 }
526 };
527 let completion = match script.run(tc) {
528 Some(result) => result,
529 None => {
530 if bridge_ctx.is_some() {
531 clear_module_state();
532 }
533 return match tc.exception() {
534 Some(e) => {
535 let (c, err) = exception_to_result(tc, e);
536 (c, Some(err))
537 }
538 None => (1, None),
539 };
540 }
541 };
542
543 tc.perform_microtask_checkpoint();
547
548 if let Some(exception) = tc.exception() {
549 if bridge_ctx.is_some() {
550 clear_module_state();
551 }
552 let (c, err) = exception_to_result(tc, exception);
553 return (c, Some(err));
554 }
555
556 if let Some(err) = take_unhandled_promise_rejection(tc) {
557 if bridge_ctx.is_some() {
558 clear_module_state();
559 }
560 return (1, Some(err));
561 }
562
563 if completion.is_promise() {
566 let promise = v8::Local::<v8::Promise>::try_from(completion).unwrap();
567 match promise.state() {
568 v8::PromiseState::Pending => {
569 set_pending_script_evaluation(tc, promise);
570 return (0, None);
571 }
572 v8::PromiseState::Rejected => {
573 let rejection = promise.result(tc);
574 if bridge_ctx.is_some() {
575 clear_module_state();
576 }
577 let (c, err) = exception_to_result(tc, rejection);
578 return (c, Some(err));
579 }
580 v8::PromiseState::Fulfilled => {
581 return (extract_global_process_exit_code(tc).unwrap_or(0), None);
582 }
583 }
584 }
585 }
586
587 (extract_global_process_exit_code(scope).unwrap_or(0), None)
588}
589
590pub fn extract_process_exit_code(
596 scope: &mut v8::HandleScope,
597 exception: v8::Local<v8::Value>,
598) -> Option<i32> {
599 if !exception.is_object() {
600 return None;
601 }
602 let obj = v8::Local::<v8::Object>::try_from(exception).ok()?;
603 let sentinel_key = v8::String::new(scope, "_isProcessExit")?;
604 let sentinel_val = obj.get(scope, sentinel_key.into())?;
605 if !sentinel_val.is_true() {
606 return None;
607 }
608 let code_key = v8::String::new(scope, "code")?;
610 let code_val = obj.get(scope, code_key.into())?;
611 if code_val.is_undefined() || code_val.is_null() {
612 Some(0)
613 } else if code_val.is_number() {
614 Some(code_val.int32_value(scope).unwrap_or(0))
615 } else {
616 Some(1)
617 }
618}
619
620fn extract_global_process_exit_code(scope: &mut v8::HandleScope) -> Option<i32> {
621 let context = scope.get_current_context();
622 let global = context.global(scope);
623 let process_key = v8::String::new(scope, "process")?;
624 let process_val = global.get(scope, process_key.into())?;
625 if !process_val.is_object() {
626 return None;
627 }
628
629 let process_obj = v8::Local::<v8::Object>::try_from(process_val).ok()?;
630 let exit_code_key = v8::String::new(scope, "exitCode")?;
631 let exit_code_val = process_obj.get(scope, exit_code_key.into())?;
632 if exit_code_val.is_undefined() || exit_code_val.is_null() {
633 None
634 } else if exit_code_val.is_number() {
635 Some(exit_code_val.int32_value(scope).unwrap_or(0))
636 } else {
637 None
638 }
639}
640
641pub(crate) fn exception_to_result(
645 scope: &mut v8::HandleScope,
646 exception: v8::Local<v8::Value>,
647) -> (i32, ExecutionError) {
648 let error = extract_error_info(scope, exception);
649 let exit_code = extract_process_exit_code(scope, exception)
650 .or_else(|| parse_process_exit_code_from_error(&error))
651 .unwrap_or(1);
652 (exit_code, error)
653}
654
655fn parse_process_exit_code_from_error(error: &ExecutionError) -> Option<i32> {
656 if error.error_type != "ProcessExitError" && !error.message.starts_with("process.exit(") {
657 return None;
658 }
659 let code = error
660 .message
661 .strip_prefix("process.exit(")?
662 .strip_suffix(')')?;
663 code.parse::<i32>().ok()
664}
665
666pub(crate) fn extract_error_info(
671 scope: &mut v8::HandleScope,
672 exception: v8::Local<v8::Value>,
673) -> ExecutionError {
674 if !exception.is_object() {
675 return ExecutionError {
677 error_type: "Error".into(),
678 message: exception.to_rust_string_lossy(scope),
679 stack: String::new(),
680 code: None,
681 };
682 }
683
684 let obj = v8::Local::<v8::Object>::try_from(exception).unwrap();
685
686 let error_type = {
688 let ctor_key = v8::String::new(scope, "constructor").unwrap();
689 let name_key = v8::String::new(scope, "name").unwrap();
690 obj.get(scope, ctor_key.into())
691 .filter(|v| v.is_object())
692 .and_then(|ctor| {
693 let ctor_obj = v8::Local::<v8::Object>::try_from(ctor).ok()?;
694 ctor_obj.get(scope, name_key.into())
695 })
696 .filter(|v| v.is_string())
697 .map(|v| v.to_rust_string_lossy(scope))
698 .filter(|n| !n.is_empty())
699 .unwrap_or_else(|| "Error".into())
700 };
701
702 let message = {
704 let msg_key = v8::String::new(scope, "message").unwrap();
705 obj.get(scope, msg_key.into())
706 .filter(|v| v.is_string())
707 .map(|v| v.to_rust_string_lossy(scope))
708 .unwrap_or_else(|| exception.to_rust_string_lossy(scope))
709 };
710
711 let stack = {
713 let stack_key = v8::String::new(scope, "stack").unwrap();
714 obj.get(scope, stack_key.into())
715 .filter(|v| v.is_string())
716 .map(|v| v.to_rust_string_lossy(scope))
717 .unwrap_or_default()
718 };
719
720 let code = {
722 let code_key = v8::String::new(scope, "code").unwrap();
723 obj.get(scope, code_key.into())
724 .filter(|v| v.is_string())
725 .map(|v| v.to_rust_string_lossy(scope))
726 };
727
728 ExecutionError {
729 error_type,
730 message,
731 stack,
732 code,
733 }
734}
735
736#[cfg(test)]
738fn build_process_config<'s>(
739 scope: &mut v8::HandleScope<'s>,
740 config: &ProcessConfig,
741) -> v8::Local<'s, v8::Object> {
742 let obj = v8::Object::new(scope);
743
744 let key = v8::String::new(scope, "cwd").unwrap();
746 let val = v8::String::new(scope, &config.cwd).unwrap();
747 obj.set(scope, key.into(), val.into());
748
749 let env_key = v8::String::new(scope, "env").unwrap();
751 let env_obj = v8::Object::new(scope);
752 for (k, v) in &config.env {
753 let ek = v8::String::new(scope, k).unwrap();
754 let ev = v8::String::new(scope, v).unwrap();
755 env_obj.set(scope, ek.into(), ev.into());
756 }
757 env_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
758 obj.set(scope, env_key.into(), env_obj.into());
759
760 let key = v8::String::new(scope, "timing_mitigation").unwrap();
762 let val = v8::String::new(scope, &config.timing_mitigation).unwrap();
763 obj.set(scope, key.into(), val.into());
764
765 let key = v8::String::new(scope, "frozen_time_ms").unwrap();
767 let val: v8::Local<v8::Value> = match config.frozen_time_ms {
768 Some(ms) => v8::Number::new(scope, ms).into(),
769 None => v8::null(scope).into(),
770 };
771 obj.set(scope, key.into(), val);
772
773 let key = v8::String::new(scope, "high_resolution_time").unwrap();
775 let val = v8::Boolean::new(scope, config.high_resolution_time);
776 obj.set(scope, key.into(), val.into());
777
778 obj
779}
780
781#[cfg(test)]
783fn build_os_config<'s>(
784 scope: &mut v8::HandleScope<'s>,
785 config: &OsConfig,
786) -> v8::Local<'s, v8::Object> {
787 let obj = v8::Object::new(scope);
788
789 for (name, value) in [
790 ("homedir", config.homedir.as_str()),
791 ("tmpdir", config.tmpdir.as_str()),
792 ("platform", config.platform.as_str()),
793 ("arch", config.arch.as_str()),
794 ] {
795 let key = v8::String::new(scope, name).unwrap();
796 let val = v8::String::new(scope, value).unwrap();
797 obj.set(scope, key.into(), val.into());
798 }
799
800 obj
801}
802
803pub trait GuestModuleReader: Send {
819 fn read_module_source(&mut self, resolved_guest_path: &str) -> Option<String>;
822
823 fn resolve_module(&mut self, specifier: &str, referrer: &str) -> Option<String> {
828 let _ = (specifier, referrer);
829 None
830 }
831}
832
833pub fn install_session_guest_reader(reader: Option<Box<dyn GuestModuleReader>>) {
838 SESSION_GUEST_READER.with(|cell| *cell.borrow_mut() = reader);
839}
840
841struct ModuleResolveState {
842 bridge_ctx: *const BridgeCallContext,
843 module_names: HashMap<NonZeroI32, String>,
845 module_cache: HashMap<String, v8::Global<v8::Module>>,
847 guest_reader: Option<Box<dyn GuestModuleReader>>,
850}
851
852unsafe impl Send for ModuleResolveState {}
856
857#[cfg_attr(test, allow(dead_code))]
864struct PendingModuleEvaluation {
865 module: v8::Global<v8::Module>,
866 promise: v8::Global<v8::Promise>,
867}
868
869unsafe impl Send for PendingModuleEvaluation {}
872
873struct PendingScriptEvaluation {
874 promise: v8::Global<v8::Promise>,
875}
876
877unsafe impl Send for PendingScriptEvaluation {}
878
879thread_local! {
880 static MODULE_RESOLVE_STATE: RefCell<Option<ModuleResolveState>> = const { RefCell::new(None) };
881 static SESSION_GUEST_READER: RefCell<Option<Box<dyn GuestModuleReader>>> = const { RefCell::new(None) };
885 static PENDING_MODULE_EVALUATION: RefCell<Option<PendingModuleEvaluation>> = const { RefCell::new(None) };
886 static PENDING_SCRIPT_EVALUATION: RefCell<Option<PendingScriptEvaluation>> = const { RefCell::new(None) };
887 static CJS_RUNTIME_EXTRACTION_IN_PROGRESS: RefCell<HashSet<String>> =
888 RefCell::new(HashSet::new());
889}
890
891const MAX_MODULE_RESOLVE_MODULES: usize = 1024;
892const MAX_MODULE_RESOLVE_CACHE_ENTRIES: usize = 4096;
893const MAX_MODULE_PREFETCH_GRAPH_MODULES: usize = 1024;
894const MAX_MODULE_PREFETCH_BATCH_SIZE: usize = 256;
895const MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES: usize = 16 * 1024 * 1024;
896const MAX_CJS_NAMED_EXPORTS: usize = 1024;
897const MAX_CJS_RUNTIME_EXPORT_NAME_LEN: usize = 512;
898
899fn module_request_cache_key(specifier: &str, referrer_name: &str) -> String {
900 format!("{}\0{}", referrer_name, specifier)
901}
902
903#[cfg_attr(test, allow(dead_code))]
904pub fn clear_module_state() {
905 MODULE_RESOLVE_STATE.with(|cell| {
906 *cell.borrow_mut() = None;
907 });
908}
909
910pub fn clear_pending_module_evaluation() {
911 PENDING_MODULE_EVALUATION.with(|cell| {
912 *cell.borrow_mut() = None;
913 });
914}
915
916pub fn clear_pending_script_evaluation() {
917 PENDING_SCRIPT_EVALUATION.with(|cell| {
918 *cell.borrow_mut() = None;
919 });
920}
921
922#[cfg_attr(test, allow(dead_code))]
923pub fn has_pending_module_evaluation() -> bool {
924 PENDING_MODULE_EVALUATION.with(|cell| cell.borrow().is_some())
925}
926
927pub fn has_pending_script_evaluation() -> bool {
928 PENDING_SCRIPT_EVALUATION.with(|cell| cell.borrow().is_some())
929}
930
931pub fn pending_module_evaluation_needs_wait(scope: &mut v8::HandleScope) -> bool {
932 PENDING_MODULE_EVALUATION.with(|cell| {
933 let borrow = cell.borrow();
934 let Some(pending) = borrow.as_ref() else {
935 return false;
936 };
937 let promise = v8::Local::new(scope, &pending.promise);
938 promise.state() == v8::PromiseState::Pending
939 })
940}
941
942pub fn pending_script_evaluation_needs_wait(scope: &mut v8::HandleScope) -> bool {
943 PENDING_SCRIPT_EVALUATION.with(|cell| {
944 let borrow = cell.borrow();
945 let Some(pending) = borrow.as_ref() else {
946 return false;
947 };
948 let promise = v8::Local::new(scope, &pending.promise);
949 promise.state() == v8::PromiseState::Pending
950 })
951}
952
953fn set_pending_module_evaluation(
954 scope: &mut v8::HandleScope,
955 module: v8::Local<v8::Module>,
956 promise: v8::Local<v8::Promise>,
957) {
958 PENDING_MODULE_EVALUATION.with(|cell| {
959 *cell.borrow_mut() = Some(PendingModuleEvaluation {
960 module: v8::Global::new(scope, module),
961 promise: v8::Global::new(scope, promise),
962 });
963 });
964}
965
966pub fn set_pending_script_evaluation(scope: &mut v8::HandleScope, promise: v8::Local<v8::Promise>) {
967 PENDING_SCRIPT_EVALUATION.with(|cell| {
968 *cell.borrow_mut() = Some(PendingScriptEvaluation {
969 promise: v8::Global::new(scope, promise),
970 });
971 });
972}
973
974pub(crate) fn take_unhandled_promise_rejection(
975 scope: &mut v8::HandleScope,
976) -> Option<ExecutionError> {
977 scope
978 .get_slot_mut::<crate::isolate::PromiseRejectState>()
979 .and_then(|state| state.take_next_unhandled())
980}
981
982pub fn finalize_pending_script_evaluation(
983 scope: &mut v8::HandleScope,
984) -> Option<(i32, Option<ExecutionError>)> {
985 let pending = PENDING_SCRIPT_EVALUATION.with(|cell| cell.borrow_mut().take())?;
986 let tc = &mut v8::TryCatch::new(scope);
987 let promise = v8::Local::new(tc, &pending.promise);
988
989 tc.perform_microtask_checkpoint();
990
991 if let Some(exception) = tc.exception() {
992 let (code, err) = exception_to_result(tc, exception);
993 return Some((code, Some(err)));
994 }
995
996 if let Some(err) = take_unhandled_promise_rejection(tc) {
997 return Some((1, Some(err)));
998 }
999
1000 match promise.state() {
1001 v8::PromiseState::Pending => {
1002 PENDING_SCRIPT_EVALUATION.with(|cell| {
1003 *cell.borrow_mut() = Some(pending);
1004 });
1005 None
1006 }
1007 v8::PromiseState::Rejected => {
1008 let rejection = promise.result(tc);
1009 let (code, err) = exception_to_result(tc, rejection);
1010 Some((code, Some(err)))
1011 }
1012 v8::PromiseState::Fulfilled => {
1013 Some((extract_global_process_exit_code(tc).unwrap_or(0), None))
1014 }
1015 }
1016}
1017
1018fn serialize_module_exports(
1019 scope: &mut v8::HandleScope,
1020 module: v8::Local<v8::Module>,
1021) -> Result<Vec<u8>, ExecutionError> {
1022 let namespace = module.get_module_namespace();
1028 let namespace_obj = namespace.to_object(scope).unwrap();
1029 let prop_names = namespace_obj
1030 .get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
1031 .unwrap();
1032 let exports_val: v8::Local<v8::Value> = if prop_names.length() == 0 {
1033 let ctx = scope.get_current_context();
1035 let global = ctx.global(scope);
1036 let module_key = v8::String::new(scope, "module").unwrap();
1037 let cjs_exports = global
1038 .get(scope, module_key.into())
1039 .and_then(|m| m.to_object(scope))
1040 .and_then(|m| {
1041 let exports_key = v8::String::new(scope, "exports").unwrap();
1042 m.get(scope, exports_key.into())
1043 })
1044 .filter(|v| !v.is_undefined() && !v.is_null_or_undefined());
1045 match cjs_exports {
1046 Some(val) => val,
1047 None => v8::Object::new(scope).into(),
1048 }
1049 } else {
1050 let plain = v8::Object::new(scope);
1051 for i in 0..prop_names.length() {
1052 let key = prop_names.get_index(scope, i).unwrap();
1053 let val = namespace_obj
1054 .get(scope, key)
1055 .unwrap_or_else(|| v8::undefined(scope).into());
1056 plain.set(scope, key, val);
1057 }
1058 plain.into()
1059 };
1060
1061 serialize_v8_value(scope, exports_val).map_err(|err| ExecutionError {
1062 error_type: "Error".into(),
1063 message: format!("failed to serialize exports: {}", err),
1064 stack: String::new(),
1065 code: None,
1066 })
1067}
1068
1069#[cfg_attr(test, allow(dead_code))]
1070pub fn finalize_pending_module_evaluation(
1071 scope: &mut v8::HandleScope,
1072) -> Option<(i32, Option<Vec<u8>>, Option<ExecutionError>)> {
1073 let pending = PENDING_MODULE_EVALUATION.with(|cell| cell.borrow_mut().take())?;
1074 let tc = &mut v8::TryCatch::new(scope);
1075 let module = v8::Local::new(tc, &pending.module);
1076 let promise = v8::Local::new(tc, &pending.promise);
1077
1078 tc.perform_microtask_checkpoint();
1079
1080 if let Some(exception) = tc.exception() {
1081 let (code, err) = exception_to_result(tc, exception);
1082 return Some((code, None, Some(err)));
1083 }
1084
1085 if let Some(err) = take_unhandled_promise_rejection(tc) {
1086 return Some((1, None, Some(err)));
1087 }
1088
1089 match promise.state() {
1090 v8::PromiseState::Pending => {
1091 PENDING_MODULE_EVALUATION.with(|cell| {
1092 *cell.borrow_mut() = Some(pending);
1093 });
1094 None
1095 }
1096 v8::PromiseState::Rejected => {
1097 let rejection = promise.result(tc);
1098 let (code, err) = exception_to_result(tc, rejection);
1099 Some((code, None, Some(err)))
1100 }
1101 v8::PromiseState::Fulfilled => {
1102 if module.get_status() == v8::ModuleStatus::Errored {
1103 let exc = module.get_exception();
1104 let (code, err) = exception_to_result(tc, exc);
1105 return Some((code, None, Some(err)));
1106 }
1107
1108 match serialize_module_exports(tc, module) {
1109 Ok(exports) => Some((0, Some(exports), None)),
1110 Err(err) => Some((1, None, Some(err))),
1111 }
1112 }
1113 }
1114}
1115
1116pub fn execute_module(
1124 scope: &mut v8::HandleScope,
1125 bridge_ctx: &BridgeCallContext,
1126 bridge_code: &str,
1127 user_code: &str,
1128 file_path: Option<&str>,
1129 bridge_cache: &mut Option<BridgeCodeCache>,
1130) -> (i32, Option<Vec<u8>>, Option<ExecutionError>) {
1131 clear_pending_module_evaluation();
1132
1133 let guest_reader = SESSION_GUEST_READER.with(|cell| cell.borrow_mut().take());
1136 MODULE_RESOLVE_STATE.with(|cell| {
1137 *cell.borrow_mut() = Some(ModuleResolveState {
1138 bridge_ctx: bridge_ctx as *const BridgeCallContext,
1139 module_names: HashMap::new(),
1140 module_cache: HashMap::new(),
1141 guest_reader,
1142 });
1143 });
1144
1145 if !bridge_code.is_empty() {
1147 let (code, err) = run_bridge_cached(scope, bridge_code, bridge_cache);
1148 if code != 0 {
1149 clear_module_state();
1150 return (code, None, err);
1151 }
1152 }
1153
1154 {
1156 let tc = &mut v8::TryCatch::new(scope);
1157 let resource_name_str = file_path.unwrap_or("<user_module>");
1158 let resource = v8::String::new(tc, resource_name_str).unwrap();
1159 let origin = v8::ScriptOrigin::new(
1160 tc,
1161 resource.into(),
1162 0,
1163 0,
1164 false,
1165 -1,
1166 None,
1167 false,
1168 false,
1169 true, None,
1171 );
1172
1173 let effective_user_code = add_esm_runtime_prelude(user_code);
1174 let v8_source = match v8::String::new(tc, &effective_user_code) {
1175 Some(s) => s,
1176 None => {
1177 clear_module_state();
1178 return (
1179 1,
1180 None,
1181 Some(ExecutionError {
1182 error_type: "Error".into(),
1183 message: "user code string too large for V8".into(),
1184 stack: String::new(),
1185 code: None,
1186 }),
1187 );
1188 }
1189 };
1190
1191 let mut source = v8::script_compiler::Source::new(v8_source, Some(&origin));
1192 let module = match v8::script_compiler::compile_module(tc, &mut source) {
1193 Some(m) => m,
1194 None => {
1195 clear_module_state();
1196 return match tc.exception() {
1197 Some(e) => {
1198 let (c, err) = exception_to_result(tc, e);
1199 (c, None, Some(err))
1200 }
1201 None => (1, None, None),
1202 };
1203 }
1204 };
1205
1206 MODULE_RESOLVE_STATE.with(|cell| {
1208 if let Some(state) = cell.borrow_mut().as_mut() {
1209 state
1210 .module_names
1211 .insert(module.get_identity_hash(), resource_name_str.to_string());
1212 }
1213 });
1214
1215 prefetch_module_imports(tc, bridge_ctx, module, resource_name_str);
1219
1220 let inst_result = module.instantiate_module(tc, module_resolve_callback);
1222 if inst_result.is_none() {
1223 clear_module_state();
1224 return match tc.exception() {
1225 Some(e) => {
1226 let (c, err) = exception_to_result(tc, e);
1227 (c, None, Some(err))
1228 }
1229 None => (1, None, None),
1230 };
1231 }
1232
1233 let eval_result = module.evaluate(tc);
1235 if eval_result.is_none() {
1236 clear_module_state();
1237 return match tc.exception() {
1238 Some(e) => {
1239 let (c, err) = exception_to_result(tc, e);
1240 (c, None, Some(err))
1241 }
1242 None => (1, None, None),
1243 };
1244 }
1245
1246 if eval_result.unwrap().is_promise() {
1252 let promise = v8::Local::<v8::Promise>::try_from(eval_result.unwrap()).unwrap();
1253 tc.perform_microtask_checkpoint();
1254
1255 if let Some(exception) = tc.exception() {
1256 clear_module_state();
1257 let (c, err) = exception_to_result(tc, exception);
1258 return (c, None, Some(err));
1259 }
1260
1261 if let Some(err) = take_unhandled_promise_rejection(tc) {
1262 clear_module_state();
1263 return (1, None, Some(err));
1264 }
1265
1266 match promise.state() {
1267 v8::PromiseState::Pending => {
1268 set_pending_module_evaluation(tc, module, promise);
1269 return (0, None, None);
1270 }
1271 v8::PromiseState::Rejected => {
1272 let rejection = promise.result(tc);
1273 clear_module_state();
1274 let (exit_code, err) = exception_to_result(tc, rejection);
1275 return (exit_code, None, Some(err));
1276 }
1277 v8::PromiseState::Fulfilled => {}
1278 }
1279 } else {
1280 tc.perform_microtask_checkpoint();
1284
1285 if let Some(exception) = tc.exception() {
1286 clear_module_state();
1287 let (c, err) = exception_to_result(tc, exception);
1288 return (c, None, Some(err));
1289 }
1290
1291 if let Some(err) = take_unhandled_promise_rejection(tc) {
1292 clear_module_state();
1293 return (1, None, Some(err));
1294 }
1295 }
1296
1297 if module.get_status() == v8::ModuleStatus::Errored {
1299 let exc = module.get_exception();
1300 clear_module_state();
1301 let (exit_code, err) = exception_to_result(tc, exc);
1302 return (exit_code, None, Some(err));
1303 }
1304
1305 let exports_bytes = match serialize_module_exports(tc, module) {
1306 Ok(bytes) => bytes,
1307 Err(err) => {
1308 clear_module_state();
1309 return (1, None, Some(err));
1310 }
1311 };
1312
1313 (0, Some(exports_bytes), None)
1316 }
1317}
1318
1319fn extract_uncached_imports(
1324 scope: &mut v8::HandleScope,
1325 module: v8::Local<v8::Module>,
1326 referrer_name: &str,
1327) -> Vec<(String, String)> {
1328 let requests = module.get_module_requests();
1329 let mut uncached = Vec::new();
1330 for i in 0..requests.length() {
1331 if uncached.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1332 break;
1333 }
1334 let data = requests.get(scope, i).unwrap();
1335 let request: v8::Local<v8::ModuleRequest> = data.cast();
1336 let specifier = request.get_specifier().to_rust_string_lossy(scope);
1337 let cache_key = module_request_cache_key(&specifier, referrer_name);
1338
1339 let already_cached = MODULE_RESOLVE_STATE.with(|cell| {
1341 let borrow = cell.borrow();
1342 let state = borrow.as_ref().unwrap();
1343 state.module_cache.contains_key(&cache_key)
1344 });
1345 if !already_cached {
1346 uncached.push((specifier, referrer_name.to_string()));
1347 }
1348 }
1349 uncached
1350}
1351
1352fn prefetch_module_imports(
1359 scope: &mut v8::HandleScope,
1360 bridge_ctx: &BridgeCallContext,
1361 root_module: v8::Local<v8::Module>,
1362 root_name: &str,
1363) {
1364 let mut pending: Vec<(v8::Global<v8::Module>, String)> =
1366 vec![(v8::Global::new(scope, root_module), root_name.to_string())];
1367 let mut visited_modules = 0usize;
1368
1369 while !pending.is_empty() && visited_modules < MAX_MODULE_PREFETCH_GRAPH_MODULES {
1370 let remaining_modules = MAX_MODULE_PREFETCH_GRAPH_MODULES - visited_modules;
1371 let current_len = pending.len().min(remaining_modules);
1372 let current: Vec<_> = pending.drain(..current_len).collect();
1373 visited_modules += current.len();
1374
1375 let mut batch: Vec<(String, String)> = Vec::new();
1377 for (global_mod, referrer) in ¤t {
1378 let local_mod = v8::Local::new(scope, global_mod);
1379 let imports = extract_uncached_imports(scope, local_mod, referrer);
1380 for (spec, ref_name) in imports {
1381 if batch.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1382 break;
1383 }
1384 if !batch.iter().any(|(s, r)| s == &spec && r == &ref_name) {
1386 batch.push((spec, ref_name));
1387 }
1388 }
1389 if batch.len() >= MAX_MODULE_PREFETCH_BATCH_SIZE {
1390 break;
1391 }
1392 }
1393
1394 if batch.is_empty() {
1395 break;
1396 }
1397
1398 let results = match batch_resolve_via_ipc(scope, bridge_ctx, &batch) {
1400 Some(r) => r,
1401 None => break, };
1403
1404 let mut next_pending: Vec<(v8::Global<v8::Module>, String)> = Vec::new();
1406 for (i, result) in results.iter().enumerate() {
1407 if i >= batch.len() {
1408 break;
1409 }
1410 if let Some((resolved_path, source_code)) = result {
1411 let already_cached = MODULE_RESOLVE_STATE.with(|cell| {
1413 let borrow = cell.borrow();
1414 let state = borrow.as_ref().unwrap();
1415 state.module_cache.contains_key(resolved_path)
1416 });
1417 if already_cached {
1418 continue;
1419 }
1420
1421 let module_format = lookup_module_format_via_ipc(scope, bridge_ctx, resolved_path);
1422 let effective_source =
1423 build_module_source(scope, source_code, resolved_path, module_format);
1424
1425 let resource = match v8::String::new(scope, resolved_path) {
1427 Some(s) => s,
1428 None => continue,
1429 };
1430 let origin = v8::ScriptOrigin::new(
1431 scope,
1432 resource.into(),
1433 0,
1434 0,
1435 false,
1436 -1,
1437 None,
1438 false,
1439 false,
1440 true, None,
1442 );
1443 let v8_source = match v8::String::new(scope, &effective_source) {
1444 Some(s) => s,
1445 None => continue,
1446 };
1447 let mut compiled = v8::script_compiler::Source::new(v8_source, Some(&origin));
1448 let module = match v8::script_compiler::compile_module(scope, &mut compiled) {
1449 Some(m) => m,
1450 None => continue,
1451 };
1452
1453 let global = v8::Global::new(scope, module);
1455 if !cache_resolved_module(
1456 module,
1457 global,
1458 resolved_path.clone(),
1459 Some(module_request_cache_key(&batch[i].0, &batch[i].1)),
1460 ) {
1461 return;
1462 }
1463
1464 if visited_modules + next_pending.len() < MAX_MODULE_PREFETCH_GRAPH_MODULES {
1465 next_pending.push((v8::Global::new(scope, module), resolved_path.clone()));
1466 }
1467 }
1468 }
1469
1470 pending = next_pending;
1471 }
1472}
1473
1474fn resolve_or_compile_module<'s>(
1475 scope: &mut v8::HandleScope<'s>,
1476 specifier_str: &str,
1477 referrer_name: &str,
1478) -> Option<v8::Local<'s, v8::Module>> {
1479 let request_cache_key = module_request_cache_key(specifier_str, referrer_name);
1480
1481 let cached_global = MODULE_RESOLVE_STATE.with(|cell| {
1483 let borrow = cell.borrow();
1484 let state = borrow.as_ref()?;
1485 state.module_cache.get(&request_cache_key).cloned()
1486 });
1487 if let Some(cached) = cached_global {
1488 return Some(v8::Local::new(scope, &cached));
1489 }
1490
1491 let bridge_ctx_ptr = MODULE_RESOLVE_STATE.with(|cell| {
1493 let borrow = cell.borrow();
1494 borrow.as_ref().map(|state| state.bridge_ctx)
1495 });
1496 let bridge_ctx_ptr = bridge_ctx_ptr?;
1497 let ctx = unsafe { &*bridge_ctx_ptr };
1498
1499 let trace = mod_trace_enabled();
1502 let t = trace.then(Instant::now);
1503 let direct_resolved = MODULE_RESOLVE_STATE.with(|cell| {
1504 cell.borrow_mut()
1505 .as_mut()
1506 .and_then(|state| state.guest_reader.as_mut())
1507 .and_then(|reader| reader.resolve_module(specifier_str, referrer_name))
1508 });
1509 let resolved_path = match direct_resolved {
1510 Some(path) => path,
1511 None => resolve_module_via_ipc(scope, ctx, specifier_str, referrer_name)?,
1512 };
1513 if let Some(t) = t {
1514 record_mod(1, t.elapsed().as_nanos() as u64);
1515 }
1516
1517 let cached_global = MODULE_RESOLVE_STATE.with(|cell| {
1519 let borrow = cell.borrow();
1520 let state = borrow.as_ref()?;
1521 state.module_cache.get(&resolved_path).cloned()
1522 });
1523 if let Some(cached) = cached_global {
1524 return Some(v8::Local::new(scope, &cached));
1525 }
1526
1527 let t = trace.then(Instant::now);
1532 let direct_source = MODULE_RESOLVE_STATE.with(|cell| {
1533 cell.borrow_mut()
1534 .as_mut()
1535 .and_then(|state| state.guest_reader.as_mut())
1536 .and_then(|reader| reader.read_module_source(&resolved_path))
1537 });
1538 let raw_source = match direct_source {
1539 Some(source) => source,
1540 None => load_module_via_ipc(scope, ctx, &resolved_path)?,
1541 };
1542 if let Some(t) = t {
1543 record_mod(2, t.elapsed().as_nanos() as u64);
1544 }
1545 let t = trace.then(Instant::now);
1546 let module_format = lookup_module_format_via_ipc(scope, ctx, &resolved_path);
1547 if let Some(t) = t {
1548 record_mod(3, t.elapsed().as_nanos() as u64);
1549 }
1550 let source_code = build_module_source(scope, &raw_source, &resolved_path, module_format);
1551
1552 let resource = v8::String::new(scope, &resolved_path)?;
1553 let origin = v8::ScriptOrigin::new(
1554 scope,
1555 resource.into(),
1556 0,
1557 0,
1558 false,
1559 -1,
1560 None,
1561 false,
1562 false,
1563 true,
1564 None,
1565 );
1566 let v8_source = match v8::String::new(scope, &source_code) {
1567 Some(s) => s,
1568 None => {
1569 throw_module_error(scope, "module source too large for V8");
1570 return None;
1571 }
1572 };
1573 let mut compiled = v8::script_compiler::Source::new(v8_source, Some(&origin));
1574 let t = trace.then(Instant::now);
1575 let module = v8::script_compiler::compile_module(scope, &mut compiled)?;
1576 if let Some(t) = t {
1577 record_mod(4, t.elapsed().as_nanos() as u64);
1578 }
1579 let global = v8::Global::new(scope, module);
1580 if !cache_resolved_module(module, global, resolved_path, Some(request_cache_key)) {
1581 throw_module_error(scope, "module resolution cache limit exceeded");
1582 return None;
1583 }
1584
1585 Some(module)
1586}
1587
1588fn cache_resolved_module(
1589 module: v8::Local<v8::Module>,
1590 global: v8::Global<v8::Module>,
1591 resolved_path: String,
1592 request_cache_key: Option<String>,
1593) -> bool {
1594 MODULE_RESOLVE_STATE.with(|cell| {
1595 let mut borrow = cell.borrow_mut();
1596 let Some(state) = borrow.as_mut() else {
1597 return true;
1598 };
1599
1600 let identity_hash = module.get_identity_hash();
1601 let new_module_name = !state.module_names.contains_key(&identity_hash);
1602 let new_resolved_path = !state.module_cache.contains_key(&resolved_path);
1603 let new_request_key = request_cache_key
1604 .as_ref()
1605 .is_some_and(|key| !state.module_cache.contains_key(key));
1606
1607 let next_module_count = state.module_names.len() + usize::from(new_module_name);
1608 let next_cache_count = state.module_cache.len()
1609 + usize::from(new_resolved_path)
1610 + usize::from(new_request_key);
1611 if next_module_count > MAX_MODULE_RESOLVE_MODULES
1612 || next_cache_count > MAX_MODULE_RESOLVE_CACHE_ENTRIES
1613 {
1614 return false;
1615 }
1616
1617 state
1618 .module_names
1619 .insert(identity_hash, resolved_path.clone());
1620 state
1621 .module_cache
1622 .insert(resolved_path.clone(), global.clone());
1623 if let Some(request_cache_key) = request_cache_key {
1624 state.module_cache.insert(request_cache_key, global);
1625 }
1626 true
1627 })
1628}
1629
1630#[cfg_attr(test, allow(dead_code))]
1633pub extern "C" fn import_meta_object_callback(
1634 context: v8::Local<v8::Context>,
1635 module: v8::Local<v8::Module>,
1636 meta: v8::Local<v8::Object>,
1637) {
1638 let scope = &mut unsafe { v8::CallbackScope::new(context) };
1639
1640 let identity_hash = module.get_identity_hash();
1643 let url_str = MODULE_RESOLVE_STATE.with(|cell| {
1644 let state_opt = cell.borrow();
1645 if let Some(ref state) = *state_opt {
1646 if let Some(name) = state.module_names.get(&identity_hash) {
1647 let n = name.clone();
1648 if n.starts_with("file://") {
1649 return Some(n);
1650 } else if n.starts_with("/") {
1651 return Some(format!("file://{}", n));
1652 } else {
1653 return Some(n);
1654 }
1655 }
1656 }
1657 None
1658 });
1659
1660 if let Some(url) = url_str {
1661 let key = v8::String::new(scope, "url").unwrap();
1662 let value = v8::String::new(scope, &url).unwrap();
1663 meta.set(scope, key.into(), value.into());
1664 }
1665}
1666
1667#[cfg_attr(test, allow(dead_code))]
1668fn dynamic_import_namespace_callback(
1669 _scope: &mut v8::HandleScope,
1670 args: v8::FunctionCallbackArguments,
1671 mut rv: v8::ReturnValue,
1672) {
1673 rv.set(args.data());
1674}
1675
1676#[cfg_attr(test, allow(dead_code))]
1677fn dynamic_import_reject_callback(
1678 scope: &mut v8::HandleScope,
1679 args: v8::FunctionCallbackArguments,
1680 mut rv: v8::ReturnValue,
1681) {
1682 let reason = args.get(0);
1683 scope.throw_exception(reason);
1684 rv.set(reason);
1685}
1686
1687#[cfg_attr(test, allow(dead_code))]
1688pub fn dynamic_import_callback<'a>(
1689 scope: &mut v8::HandleScope<'a>,
1690 _host_defined_options: v8::Local<'a, v8::Data>,
1691 resource_name: v8::Local<'a, v8::Value>,
1692 specifier: v8::Local<'a, v8::String>,
1693 _import_attributes: v8::Local<'a, v8::FixedArray>,
1694) -> Option<v8::Local<'a, v8::Promise>> {
1695 let tc = &mut v8::TryCatch::new(scope);
1696
1697 let specifier_str = specifier.to_rust_string_lossy(tc);
1698 let referrer_name = resolve_dynamic_import_referrer_name(tc, resource_name);
1699 let module = match resolve_or_compile_module(tc, &specifier_str, &referrer_name) {
1700 Some(module) => module,
1701 None => {
1702 let reason = if let Some(exception) = tc.exception() {
1703 exception
1704 } else {
1705 let msg = v8::String::new(tc, "Cannot dynamically import module").unwrap();
1706 v8::Exception::error(tc, msg)
1707 };
1708 return rejected_promise(tc, reason);
1709 }
1710 };
1711
1712 if module.get_status() == v8::ModuleStatus::Uninstantiated
1713 && module
1714 .instantiate_module(tc, module_resolve_callback)
1715 .is_none()
1716 {
1717 let reason = if let Some(exception) = tc.exception() {
1718 exception
1719 } else {
1720 let msg =
1721 v8::String::new(tc, "Cannot instantiate dynamically imported module").unwrap();
1722 v8::Exception::error(tc, msg)
1723 };
1724 return rejected_promise(tc, reason);
1725 }
1726
1727 if module.get_status() == v8::ModuleStatus::Errored {
1728 let exception = v8::Global::new(tc, module.get_exception());
1729 let exception = v8::Local::new(tc, &exception);
1730 return rejected_promise(tc, exception);
1731 }
1732
1733 if module.get_status() == v8::ModuleStatus::Evaluated {
1734 let namespace = v8::Global::new(tc, module.get_module_namespace());
1735 let namespace = v8::Local::new(tc, &namespace);
1736 return resolved_promise(tc, namespace);
1737 }
1738
1739 let eval_result = match module.evaluate(tc) {
1740 Some(result) => result,
1741 None => {
1742 let reason = if let Some(exception) = tc.exception() {
1743 exception
1744 } else {
1745 let msg =
1746 v8::String::new(tc, "Cannot evaluate dynamically imported module").unwrap();
1747 v8::Exception::error(tc, msg)
1748 };
1749 return rejected_promise(tc, reason);
1750 }
1751 };
1752
1753 let namespace = v8::Global::new(tc, module.get_module_namespace());
1754 let namespace = v8::Local::new(tc, &namespace);
1755 if eval_result.is_promise() {
1756 let eval_promise = v8::Local::<v8::Promise>::try_from(eval_result).ok()?;
1757 let on_fulfilled = v8::FunctionTemplate::builder(dynamic_import_namespace_callback)
1758 .data(namespace)
1759 .build(tc)
1760 .get_function(tc)?;
1761 let on_rejected = v8::FunctionTemplate::builder(dynamic_import_reject_callback)
1762 .build(tc)
1763 .get_function(tc)?;
1764 return eval_promise.then2(tc, on_fulfilled, on_rejected);
1765 }
1766
1767 resolved_promise(tc, namespace)
1768}
1769
1770fn resolve_dynamic_import_referrer_name(
1771 scope: &mut v8::HandleScope,
1772 resource_name: v8::Local<v8::Value>,
1773) -> String {
1774 let candidate = resource_name.to_rust_string_lossy(scope);
1775 if candidate.starts_with('/') || candidate.starts_with("file://") {
1776 return candidate;
1777 }
1778
1779 let context = scope.get_current_context();
1780 let global = context.global(scope);
1781 let key = match v8::String::new(scope, "_currentModule") {
1782 Some(key) => key,
1783 None => return candidate,
1784 };
1785 let current_module = match global.get(scope, key.into()) {
1786 Some(value) if value.is_object() => value,
1787 _ => return candidate,
1788 };
1789 let current_module = match v8::Local::<v8::Object>::try_from(current_module) {
1790 Ok(object) => object,
1791 Err(_) => return candidate,
1792 };
1793 let filename_key = match v8::String::new(scope, "filename") {
1794 Some(key) => key,
1795 None => return candidate,
1796 };
1797 match current_module.get(scope, filename_key.into()) {
1798 Some(value) if value.is_string() => value.to_rust_string_lossy(scope),
1799 _ => candidate,
1800 }
1801}
1802
1803#[cfg_attr(test, allow(dead_code))]
1804fn resolved_promise<'s>(
1805 scope: &mut v8::HandleScope<'s>,
1806 value: v8::Local<'s, v8::Value>,
1807) -> Option<v8::Local<'s, v8::Promise>> {
1808 let resolver = v8::PromiseResolver::new(scope)?;
1809 resolver.resolve(scope, value);
1810 Some(resolver.get_promise(scope))
1811}
1812
1813#[cfg_attr(test, allow(dead_code))]
1814fn rejected_promise<'s>(
1815 scope: &mut v8::HandleScope<'s>,
1816 reason: v8::Local<'s, v8::Value>,
1817) -> Option<v8::Local<'s, v8::Promise>> {
1818 let resolver = v8::PromiseResolver::new(scope)?;
1819 resolver.reject(scope, reason);
1820 Some(resolver.get_promise(scope))
1821}
1822
1823fn batch_resolve_via_ipc(
1829 scope: &mut v8::HandleScope,
1830 ctx: &BridgeCallContext,
1831 batch: &[(String, String)],
1832) -> Option<Vec<Option<(String, String)>>> {
1833 let inner = v8::Array::new(scope, batch.len() as i32);
1836 for (i, (specifier, referrer)) in batch.iter().enumerate() {
1837 let pair = v8::Array::new(scope, 2);
1838 let spec_v8 = v8::String::new(scope, specifier)?;
1839 let ref_v8 = v8::String::new(scope, referrer)?;
1840 pair.set_index(scope, 0, spec_v8.into());
1841 pair.set_index(scope, 1, ref_v8.into());
1842 inner.set_index(scope, i as u32, pair.into());
1843 }
1844 let outer = v8::Array::new(scope, 1);
1845 outer.set_index(scope, 0, inner.into());
1846 let args = serialize_v8_value(scope, outer.into()).ok()?;
1847
1848 let response = ctx.sync_call("_batchResolveModules", args).ok()??;
1849 if response.len() > MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES {
1850 return None;
1851 }
1852 let val = deserialize_v8_value(scope, &response).ok()?;
1853
1854 let result_arr = v8::Local::<v8::Array>::try_from(val).ok()?;
1856 let mut results = Vec::with_capacity(batch.len());
1857 for i in 0..result_arr.length().min(batch.len() as u32) {
1858 let entry = result_arr.get_index(scope, i);
1859 match entry {
1860 Some(v) if !v.is_null() && !v.is_undefined() => {
1861 let obj = v8::Local::<v8::Object>::try_from(v).ok();
1862 if let Some(obj) = obj {
1863 let r_key = v8::String::new(scope, "resolved").unwrap();
1864 let s_key = v8::String::new(scope, "source").unwrap();
1865 let resolved = obj
1866 .get(scope, r_key.into())
1867 .filter(|v| v.is_string())
1868 .map(|v| v.to_rust_string_lossy(scope));
1869 let source = obj
1870 .get(scope, s_key.into())
1871 .filter(|v| v.is_string())
1872 .map(|v| v.to_rust_string_lossy(scope));
1873 match (resolved, source) {
1874 (Some(r), Some(s)) => results.push(Some((r, s))),
1875 _ => results.push(None),
1876 }
1877 } else {
1878 results.push(None);
1879 }
1880 }
1881 _ => results.push(None),
1882 }
1883 }
1884 Some(results)
1885}
1886
1887fn module_resolve_callback<'a>(
1892 context: v8::Local<'a, v8::Context>,
1893 specifier: v8::Local<'a, v8::String>,
1894 _import_attributes: v8::Local<'a, v8::FixedArray>,
1895 referrer: v8::Local<'a, v8::Module>,
1896) -> Option<v8::Local<'a, v8::Module>> {
1897 let scope = &mut unsafe { v8::CallbackScope::new(context) };
1899
1900 let specifier_str = specifier.to_rust_string_lossy(scope);
1901 let referrer_hash = referrer.get_identity_hash();
1902
1903 let referrer_name = MODULE_RESOLVE_STATE.with(|cell| {
1904 let borrow = cell.borrow();
1905 let state = borrow.as_ref()?;
1906 state.module_names.get(&referrer_hash).cloned()
1907 });
1908 let referrer_name = referrer_name?;
1909 resolve_or_compile_module(scope, &specifier_str, &referrer_name)
1910}
1911
1912fn resolve_module_via_ipc(
1914 scope: &mut v8::HandleScope,
1915 ctx: &BridgeCallContext,
1916 specifier: &str,
1917 referrer: &str,
1918) -> Option<String> {
1919 let spec_v8 = v8::String::new(scope, specifier).unwrap();
1921 let ref_v8 = v8::String::new(scope, referrer).unwrap();
1922 let arr = v8::Array::new(scope, 2);
1923 arr.set_index(scope, 0, spec_v8.into());
1924 arr.set_index(scope, 1, ref_v8.into());
1925 let args = match serialize_v8_value(scope, arr.into()) {
1926 Ok(bytes) => bytes,
1927 Err(e) => {
1928 throw_module_error(scope, &format!("_resolveModule serialize error: {}", e));
1929 return None;
1930 }
1931 };
1932
1933 match ctx.sync_call("_resolveModule", args) {
1934 Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
1935 Ok(val) => {
1936 if val.is_string() {
1937 Some(val.to_rust_string_lossy(scope))
1938 } else {
1939 throw_module_error(
1952 scope,
1953 &format!(
1954 "Cannot resolve module '{specifier}' (imported from \
1955 '{referrer}'): not found. For a bare package, ensure it is \
1956 installed in a node_modules directory on an ancestor of the \
1957 importer (or bundle the entrypoint). If you mounted a host \
1958 node_modules, point it at a directory that contains every \
1959 symlink target (e.g. the workspace root): symlinks that \
1960 escape the mount root are not followed."
1961 ),
1962 );
1963 None
1964 }
1965 }
1966 Err(e) => {
1967 throw_module_error(scope, &format!("_resolveModule decode error: {}", e));
1968 None
1969 }
1970 },
1971 Ok(None) => {
1972 throw_module_error(scope, &format!("Cannot resolve module '{}'", specifier));
1973 None
1974 }
1975 Err(e) => {
1976 throw_module_error(scope, &e);
1977 None
1978 }
1979 }
1980}
1981
1982fn load_module_via_ipc(
1984 scope: &mut v8::HandleScope,
1985 ctx: &BridgeCallContext,
1986 resolved_path: &str,
1987) -> Option<String> {
1988 let path_v8 = v8::String::new(scope, resolved_path).unwrap();
1990 let arr = v8::Array::new(scope, 1);
1991 arr.set_index(scope, 0, path_v8.into());
1992 let args = match serialize_v8_value(scope, arr.into()) {
1993 Ok(bytes) => bytes,
1994 Err(e) => {
1995 throw_module_error(scope, &format!("_loadFile serialize error: {}", e));
1996 return None;
1997 }
1998 };
1999
2000 let ipc_result = ctx.sync_call("_loadFile", args);
2001 match ipc_result {
2002 Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
2003 Ok(val) => {
2004 if val.is_string() {
2005 Some(val.to_rust_string_lossy(scope))
2006 } else {
2007 throw_module_error(
2008 scope,
2009 &format!("_loadFile returned non-string for '{}'", resolved_path),
2010 );
2011 None
2012 }
2013 }
2014 Err(e) => {
2015 throw_module_error(scope, &format!("_loadFile decode error: {}", e));
2016 None
2017 }
2018 },
2019 Ok(None) => {
2020 throw_module_error(scope, &format!("Cannot load module '{}'", resolved_path));
2021 None
2022 }
2023 Err(e) => {
2024 throw_module_error(scope, &e);
2025 None
2026 }
2027 }
2028}
2029
2030#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2031enum ResolvedModuleFormat {
2032 Module,
2033 Commonjs,
2034 Json,
2035}
2036
2037fn lookup_module_format_via_ipc(
2038 scope: &mut v8::HandleScope,
2039 ctx: &BridgeCallContext,
2040 resolved_path: &str,
2041) -> Option<ResolvedModuleFormat> {
2042 let path_v8 = v8::String::new(scope, resolved_path).unwrap();
2043 let arr = v8::Array::new(scope, 1);
2044 arr.set_index(scope, 0, path_v8.into());
2045 let args = match serialize_v8_value(scope, arr.into()) {
2046 Ok(bytes) => bytes,
2047 Err(e) => {
2048 throw_module_error(scope, &format!("_moduleFormat serialize error: {}", e));
2049 return None;
2050 }
2051 };
2052
2053 match ctx.sync_call("_moduleFormat", args) {
2054 Ok(Some(bytes)) => match deserialize_v8_value(scope, &bytes) {
2055 Ok(val) if val.is_string() => match val.to_rust_string_lossy(scope).as_str() {
2056 "module" => Some(ResolvedModuleFormat::Module),
2057 "commonjs" => Some(ResolvedModuleFormat::Commonjs),
2058 "json" => Some(ResolvedModuleFormat::Json),
2059 _ => None,
2060 },
2061 Ok(val) if val.is_null_or_undefined() => None,
2062 Ok(_) => {
2063 throw_module_error(
2064 scope,
2065 &format!("_moduleFormat returned non-string for '{}'", resolved_path),
2066 );
2067 None
2068 }
2069 Err(e) => {
2070 throw_module_error(scope, &format!("_moduleFormat decode error: {}", e));
2071 None
2072 }
2073 },
2074 Ok(None) => None,
2075 Err(e) => {
2076 throw_module_error(scope, &e);
2077 None
2078 }
2079 }
2080}
2081
2082fn throw_module_error(scope: &mut v8::HandleScope, message: &str) {
2084 let msg = v8::String::new(scope, message).unwrap();
2085 let exc = v8::Exception::error(scope, msg);
2086 scope.throw_exception(exc);
2087}
2088
2089fn strip_leading_shebang(source: &str) -> &str {
2096 match source.strip_prefix("#!") {
2097 Some(rest) => match rest.find('\n') {
2098 Some(idx) => &rest[idx..],
2099 None => "",
2100 },
2101 None => source,
2102 }
2103}
2104
2105fn build_module_source(
2106 scope: &mut v8::HandleScope,
2107 raw_source: &str,
2108 resolved_path: &str,
2109 module_format: Option<ResolvedModuleFormat>,
2110) -> String {
2111 let raw_source = strip_leading_shebang(raw_source);
2112 let normalized_path = resolved_path.to_ascii_lowercase();
2113 if normalized_path.ends_with(".json") || module_format == Some(ResolvedModuleFormat::Json) {
2114 return build_json_esm_shim(resolved_path);
2115 }
2116 if (module_format == Some(ResolvedModuleFormat::Commonjs)
2117 && !has_probable_esm_syntax(raw_source))
2118 || is_likely_cjs(raw_source, resolved_path, module_format)
2119 {
2120 return build_cjs_esm_shim(scope, raw_source, resolved_path);
2121 }
2122 add_esm_runtime_prelude(raw_source)
2123}
2124
2125fn build_json_esm_shim(resolved_path: &str) -> String {
2126 format!(
2127 "const _jsonModule = globalThis._requireFrom({}, \"/\");\nexport default _jsonModule;\n",
2128 quoted_module_path(resolved_path)
2129 )
2130}
2131
2132fn build_cjs_esm_shim(
2133 scope: &mut v8::HandleScope,
2134 raw_source: &str,
2135 resolved_path: &str,
2136) -> String {
2137 let mut names = extract_cjs_export_names(raw_source)
2148 .into_iter()
2149 .collect::<HashSet<_>>();
2150 if names.is_empty() || source_has_dynamic_cjs_reexports(raw_source) {
2151 names.extend(extract_runtime_cjs_export_names(scope, resolved_path));
2152 }
2153
2154 let mut exports = names.into_iter().collect::<Vec<_>>();
2155 exports.sort();
2156 exports.truncate(MAX_CJS_NAMED_EXPORTS);
2157
2158 let mut shim = format!(
2159 "const _cjsModule = globalThis._requireFrom({}, \"/\");\nexport default _cjsModule;\n",
2160 quoted_module_path(resolved_path)
2161 );
2162 for name in exports {
2163 shim.push_str(&format!(
2164 "export const {} = _cjsModule[\"{}\"];\n",
2165 name, name
2166 ));
2167 }
2168 shim
2169}
2170
2171fn extract_runtime_cjs_export_names(
2178 scope: &mut v8::HandleScope,
2179 resolved_path: &str,
2180) -> Vec<String> {
2181 let already_in_progress = CJS_RUNTIME_EXTRACTION_IN_PROGRESS.with(|cell| {
2182 let mut in_progress = cell.borrow_mut();
2183 !in_progress.insert(resolved_path.to_string())
2184 });
2185 if already_in_progress {
2186 return Vec::new();
2187 }
2188 let names = extract_runtime_cjs_export_names_inner(scope, resolved_path);
2189 CJS_RUNTIME_EXTRACTION_IN_PROGRESS.with(|cell| {
2190 cell.borrow_mut().remove(resolved_path);
2191 });
2192 names
2193}
2194
2195fn extract_runtime_cjs_export_names_inner(
2196 scope: &mut v8::HandleScope,
2197 resolved_path: &str,
2198) -> Vec<String> {
2199 let tc = &mut v8::TryCatch::new(scope);
2200 let context = tc.get_current_context();
2201 let global = context.global(tc);
2202
2203 let require_key = match v8::String::new(tc, "_requireFrom") {
2204 Some(key) => key,
2205 None => return Vec::new(),
2206 };
2207 let require_fn = match global
2208 .get(tc, require_key.into())
2209 .and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
2210 {
2211 Some(function) => function,
2212 None => return Vec::new(),
2213 };
2214
2215 let module_path = match v8::String::new(tc, resolved_path) {
2216 Some(path) => path,
2217 None => return Vec::new(),
2218 };
2219 let root = match v8::String::new(tc, "/") {
2220 Some(path) => path,
2221 None => return Vec::new(),
2222 };
2223 let require_args = [module_path.into(), root.into()];
2224 let receiver = v8::undefined(tc).into();
2225 let required_module = match require_fn.call(tc, receiver, &require_args) {
2226 Some(value) => value,
2227 None => return Vec::new(),
2228 };
2229 if required_module.is_null_or_undefined() || !required_module.is_object() {
2230 return Vec::new();
2231 }
2232
2233 let object_key = match v8::String::new(tc, "Object") {
2234 Some(key) => key,
2235 None => return Vec::new(),
2236 };
2237 let object_ctor = match global
2238 .get(tc, object_key.into())
2239 .and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
2240 {
2241 Some(object) => object,
2242 None => return Vec::new(),
2243 };
2244
2245 let keys_key = match v8::String::new(tc, "keys") {
2246 Some(key) => key,
2247 None => return Vec::new(),
2248 };
2249 let keys_fn = match object_ctor
2250 .get(tc, keys_key.into())
2251 .and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
2252 {
2253 Some(function) => function,
2254 None => return Vec::new(),
2255 };
2256
2257 let keys_args = [required_module];
2258 let keys = match keys_fn
2259 .call(tc, object_ctor.into(), &keys_args)
2260 .and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
2261 {
2262 Some(array) => array,
2263 None => return Vec::new(),
2264 };
2265
2266 let mut names = Vec::new();
2267 for index in 0..keys.length() {
2268 if names.len() >= MAX_CJS_NAMED_EXPORTS {
2269 break;
2270 }
2271 let Some(value) = keys.get_index(tc, index) else {
2272 continue;
2273 };
2274 if !value.is_string() {
2275 continue;
2276 }
2277 let name = value.to_rust_string_lossy(tc);
2278 if name.len() > MAX_CJS_RUNTIME_EXPORT_NAME_LEN {
2279 continue;
2280 }
2281 if is_valid_js_ident(&name) && name != "default" && name != "__esModule" {
2282 names.push(name);
2283 }
2284 }
2285 names.sort();
2286 names.dedup();
2287 names
2288}
2289
2290fn quoted_module_path(resolved_path: &str) -> String {
2291 format!(
2292 "\"{}\"",
2293 resolved_path.replace('\\', "\\\\").replace('"', "\\\"")
2294 )
2295}
2296
2297fn is_likely_cjs(
2298 source: &str,
2299 resolved_path: &str,
2300 module_format: Option<ResolvedModuleFormat>,
2301) -> bool {
2302 let normalized_path = resolved_path.to_ascii_lowercase();
2303 if normalized_path.ends_with(".mjs") || normalized_path.ends_with(".mts") {
2304 return false;
2305 }
2306 if normalized_path.ends_with(".cjs") || normalized_path.ends_with(".cts") {
2307 return true;
2308 }
2309 if module_format == Some(ResolvedModuleFormat::Module) {
2310 return false;
2311 }
2312 if has_probable_esm_syntax(source) {
2313 return false;
2314 }
2315 source.contains("module.exports") || source.contains("exports.") || source.contains("require(")
2317}
2318
2319fn has_probable_esm_syntax(source: &str) -> bool {
2320 #[derive(Clone, Copy, PartialEq, Eq)]
2321 enum ScanState {
2322 Code,
2323 LineComment,
2324 BlockComment,
2325 SingleQuote,
2326 DoubleQuote,
2327 Template,
2328 }
2329
2330 let bytes = source.as_bytes();
2331 let mut state = ScanState::Code;
2332 let mut index = 0usize;
2333 let mut brace_depth = 0u32;
2334 let mut paren_depth = 0u32;
2335 let mut bracket_depth = 0u32;
2336
2337 while index < bytes.len() {
2338 let byte = bytes[index];
2339 let next = bytes.get(index + 1).copied();
2340
2341 match state {
2342 ScanState::Code => {
2343 if index == 0 && byte == b'#' && next == Some(b'!') {
2344 state = ScanState::LineComment;
2345 index += 2;
2346 continue;
2347 }
2348 if byte == b'/' && next == Some(b'/') {
2349 state = ScanState::LineComment;
2350 index += 2;
2351 continue;
2352 }
2353 if byte == b'/' && next == Some(b'*') {
2354 state = ScanState::BlockComment;
2355 index += 2;
2356 continue;
2357 }
2358 if byte == b'\'' {
2359 state = ScanState::SingleQuote;
2360 index += 1;
2361 continue;
2362 }
2363 if byte == b'"' {
2364 state = ScanState::DoubleQuote;
2365 index += 1;
2366 continue;
2367 }
2368 if byte == b'`' {
2369 state = ScanState::Template;
2370 index += 1;
2371 continue;
2372 }
2373
2374 match byte {
2375 b'{' => brace_depth = brace_depth.saturating_add(1),
2376 b'}' => brace_depth = brace_depth.saturating_sub(1),
2377 b'(' => paren_depth = paren_depth.saturating_add(1),
2378 b')' => paren_depth = paren_depth.saturating_sub(1),
2379 b'[' => bracket_depth = bracket_depth.saturating_add(1),
2380 b']' => bracket_depth = bracket_depth.saturating_sub(1),
2381 _ => {}
2382 }
2383
2384 if brace_depth == 0
2385 && paren_depth == 0
2386 && bracket_depth == 0
2387 && is_js_ident_start(byte)
2388 {
2389 let start = index;
2390 index += 1;
2391 while index < bytes.len() && is_js_ident_continue(bytes[index]) {
2392 index += 1;
2393 }
2394
2395 let token = &source[start..index];
2396 if token == "export" {
2397 return true;
2398 }
2399 if token == "import" {
2400 let mut cursor = index;
2401 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
2402 cursor += 1;
2403 }
2404 if bytes.get(cursor).copied() != Some(b'(') {
2405 return true;
2406 }
2407 }
2408
2409 continue;
2410 }
2411
2412 index += 1;
2413 }
2414 ScanState::LineComment => {
2415 if byte == b'\n' {
2416 state = ScanState::Code;
2417 }
2418 index += 1;
2419 }
2420 ScanState::BlockComment => {
2421 if byte == b'*' && next == Some(b'/') {
2422 state = ScanState::Code;
2423 index += 2;
2424 } else {
2425 index += 1;
2426 }
2427 }
2428 ScanState::SingleQuote => {
2429 if byte == b'\\' {
2430 index += 2;
2431 } else if byte == b'\'' {
2432 state = ScanState::Code;
2433 index += 1;
2434 } else {
2435 index += 1;
2436 }
2437 }
2438 ScanState::DoubleQuote => {
2439 if byte == b'\\' {
2440 index += 2;
2441 } else if byte == b'"' {
2442 state = ScanState::Code;
2443 index += 1;
2444 } else {
2445 index += 1;
2446 }
2447 }
2448 ScanState::Template => {
2449 if byte == b'\\' {
2450 index += 2;
2451 } else if byte == b'`' {
2452 state = ScanState::Code;
2453 index += 1;
2454 } else {
2455 index += 1;
2456 }
2457 }
2458 }
2459 }
2460
2461 false
2462}
2463
2464fn is_js_ident_start(byte: u8) -> bool {
2465 byte.is_ascii_alphabetic() || byte == b'_' || byte == b'$'
2466}
2467
2468fn is_js_ident_continue(byte: u8) -> bool {
2469 is_js_ident_start(byte) || byte.is_ascii_digit()
2470}
2471
2472fn extract_cjs_export_names(source: &str) -> Vec<String> {
2475 let mut names = HashSet::new();
2476
2477 collect_cjs_property_assignment_names(source, &mut names);
2478 collect_cjs_define_property_names(source, &mut names);
2479 collect_cjs_object_literal_export_names(source, &mut names);
2480
2481 let mut result: Vec<String> = names.into_iter().collect();
2482 result.sort();
2483 result
2484}
2485
2486fn collect_cjs_property_assignment_names(
2487 source: &str,
2488 names: &mut std::collections::HashSet<String>,
2489) {
2490 for prefix in ["exports.", "module.exports."] {
2491 let mut cursor = 0usize;
2492 while names.len() < MAX_CJS_NAMED_EXPORTS {
2493 let Some(start) = find_code_pattern(source, prefix, cursor) else {
2494 break;
2495 };
2496 let name_start = start + prefix.len();
2497 let mut index = name_start;
2498 while source
2499 .as_bytes()
2500 .get(index)
2501 .is_some_and(|byte| is_js_ident_continue(*byte))
2502 {
2503 index += 1;
2504 }
2505 let name = &source[name_start..index];
2506 let next = skip_ascii_whitespace(source, index);
2507 if source.as_bytes().get(next) == Some(&b'=')
2508 && is_valid_js_ident(name)
2509 && name != "default"
2510 && name != "__esModule"
2511 {
2512 names.insert(name.to_string());
2513 }
2514 cursor = index.max(start + prefix.len());
2515 }
2516 }
2517}
2518
2519fn collect_cjs_define_property_names(source: &str, names: &mut std::collections::HashSet<String>) {
2520 let mut cursor = 0usize;
2521 while names.len() < MAX_CJS_NAMED_EXPORTS {
2522 let Some(start) = find_code_pattern(source, "Object.defineProperty", cursor) else {
2523 break;
2524 };
2525 let mut index = skip_ascii_whitespace(source, start + "Object.defineProperty".len());
2526 if source.as_bytes().get(index) != Some(&b'(') {
2527 cursor = start + "Object.defineProperty".len();
2528 continue;
2529 }
2530 index = skip_ascii_whitespace(source, index + 1);
2531 if !source.as_bytes()[index..].starts_with(b"exports") {
2532 cursor = start + "Object.defineProperty".len();
2533 continue;
2534 }
2535 index = skip_ascii_whitespace(source, index + "exports".len());
2536 if source.as_bytes().get(index) != Some(&b',') {
2537 cursor = start + "Object.defineProperty".len();
2538 continue;
2539 }
2540 index = skip_ascii_whitespace(source, index + 1);
2541 if let Some((name, end)) = parse_quoted_string_literal(source, index) {
2542 if is_valid_js_ident(name) && name != "default" && name != "__esModule" {
2543 names.insert(name.to_string());
2544 cursor = end;
2545 continue;
2546 }
2547 }
2548 cursor = start + "Object.defineProperty".len();
2549 }
2550}
2551
2552fn collect_cjs_object_literal_export_names(
2553 source: &str,
2554 names: &mut std::collections::HashSet<String>,
2555) {
2556 collect_module_exports_assignments(source, names);
2557 collect_object_assign_module_exports(source, names);
2558}
2559
2560fn collect_module_exports_assignments(source: &str, names: &mut std::collections::HashSet<String>) {
2561 let mut cursor = 0usize;
2562 while names.len() < MAX_CJS_NAMED_EXPORTS {
2563 let Some(start) = find_code_pattern(source, "module.exports", cursor) else {
2564 break;
2565 };
2566 let mut index = skip_ascii_whitespace(source, start + "module.exports".len());
2567 if source.as_bytes().get(index) != Some(&b'=') {
2568 cursor = start + "module.exports".len();
2569 continue;
2570 }
2571 index = skip_ascii_whitespace(source, index + 1);
2572 cursor = if source.as_bytes().get(index) == Some(&b'{') {
2573 collect_object_literal_keys(source, index, names)
2574 } else {
2575 index.saturating_add(1)
2576 };
2577 }
2578}
2579
2580fn collect_object_assign_module_exports(
2581 source: &str,
2582 names: &mut std::collections::HashSet<String>,
2583) {
2584 let mut cursor = 0usize;
2585 while names.len() < MAX_CJS_NAMED_EXPORTS {
2586 let Some(start) = find_code_pattern(source, "Object.assign", cursor) else {
2587 break;
2588 };
2589 let mut index = skip_ascii_whitespace(source, start + "Object.assign".len());
2590 if source.as_bytes().get(index) != Some(&b'(') {
2591 cursor = start + "Object.assign".len();
2592 continue;
2593 }
2594 index = skip_ascii_whitespace(source, index + 1);
2595 if !source.as_bytes()[index..].starts_with(b"module.exports") {
2596 cursor = start + "Object.assign".len();
2597 continue;
2598 }
2599 index = skip_ascii_whitespace(source, index + "module.exports".len());
2600 if source.as_bytes().get(index) != Some(&b',') {
2601 cursor = start + "Object.assign".len();
2602 continue;
2603 }
2604 index = skip_ascii_whitespace(source, index + 1);
2605 cursor = if source.as_bytes().get(index) == Some(&b'{') {
2606 collect_object_literal_keys(source, index, names)
2607 } else {
2608 index.saturating_add(1)
2609 };
2610 }
2611}
2612
2613#[derive(Clone, Copy, PartialEq, Eq)]
2614enum CjsScanState {
2615 Code,
2616 LineComment,
2617 BlockComment,
2618 SingleQuote,
2619 DoubleQuote,
2620 Template,
2621 Regex,
2622 RegexClass,
2623}
2624
2625fn find_code_pattern(source: &str, pattern: &str, cursor: usize) -> Option<usize> {
2626 let bytes = source.as_bytes();
2627 let mut state = CjsScanState::Code;
2628 let mut index = cursor;
2629 while index < bytes.len() {
2630 let byte = bytes[index];
2631 let next = bytes.get(index + 1).copied();
2632
2633 match state {
2634 CjsScanState::Code => {
2635 if byte == b'/' && next == Some(b'/') {
2636 state = CjsScanState::LineComment;
2637 index += 2;
2638 continue;
2639 }
2640 if byte == b'/' && next == Some(b'*') {
2641 state = CjsScanState::BlockComment;
2642 index += 2;
2643 continue;
2644 }
2645 if byte == b'\'' {
2646 state = CjsScanState::SingleQuote;
2647 index += 1;
2648 continue;
2649 }
2650 if byte == b'"' {
2651 state = CjsScanState::DoubleQuote;
2652 index += 1;
2653 continue;
2654 }
2655 if byte == b'`' {
2656 state = CjsScanState::Template;
2657 index += 1;
2658 continue;
2659 }
2660 if byte == b'/' && slash_starts_regex_literal(source, index) {
2661 state = CjsScanState::Regex;
2662 index += 1;
2663 continue;
2664 }
2665 if bytes[index..].starts_with(pattern.as_bytes())
2666 && has_code_pattern_boundary(source, index, pattern)
2667 {
2668 return Some(index);
2669 }
2670 index += 1;
2671 }
2672 CjsScanState::LineComment => {
2673 if byte == b'\n' {
2674 state = CjsScanState::Code;
2675 }
2676 index += 1;
2677 }
2678 CjsScanState::BlockComment => {
2679 if byte == b'*' && next == Some(b'/') {
2680 state = CjsScanState::Code;
2681 index += 2;
2682 } else {
2683 index += 1;
2684 }
2685 }
2686 CjsScanState::SingleQuote => {
2687 if byte == b'\\' {
2688 index += 2;
2689 } else if byte == b'\'' {
2690 state = CjsScanState::Code;
2691 index += 1;
2692 } else {
2693 index += 1;
2694 }
2695 }
2696 CjsScanState::DoubleQuote => {
2697 if byte == b'\\' {
2698 index += 2;
2699 } else if byte == b'"' {
2700 state = CjsScanState::Code;
2701 index += 1;
2702 } else {
2703 index += 1;
2704 }
2705 }
2706 CjsScanState::Template => {
2707 if byte == b'\\' {
2708 index += 2;
2709 } else if byte == b'`' {
2710 state = CjsScanState::Code;
2711 index += 1;
2712 } else {
2713 index += 1;
2714 }
2715 }
2716 CjsScanState::Regex => {
2717 if byte == b'\\' {
2718 index += 2;
2719 } else if byte == b'[' {
2720 state = CjsScanState::RegexClass;
2721 index += 1;
2722 } else if byte == b'/' {
2723 state = CjsScanState::Code;
2724 index += 1;
2725 } else {
2726 index += 1;
2727 }
2728 }
2729 CjsScanState::RegexClass => {
2730 if byte == b'\\' {
2731 index += 2;
2732 } else if byte == b']' {
2733 state = CjsScanState::Regex;
2734 index += 1;
2735 } else {
2736 index += 1;
2737 }
2738 }
2739 }
2740 }
2741 None
2742}
2743
2744fn slash_starts_regex_literal(source: &str, slash_index: usize) -> bool {
2745 let bytes = source.as_bytes();
2746 let mut cursor = slash_index;
2747 while cursor > 0 {
2748 cursor -= 1;
2749 if bytes[cursor].is_ascii_whitespace() {
2750 continue;
2751 }
2752 return match bytes[cursor] {
2753 b'(' | b')' | b'[' | b'{' | b'}' | b':' | b',' | b';' | b'=' | b'!' | b'?' | b'&'
2754 | b'|' | b'+' | b'-' | b'*' | b'%' | b'^' | b'~' | b'<' => true,
2755 b'>' => cursor > 0 && bytes[cursor - 1] == b'=',
2756 byte if is_js_ident_continue(byte) => {
2757 let end = cursor + 1;
2758 let mut start = cursor;
2759 while start > 0 && is_js_ident_continue(bytes[start - 1]) {
2760 start -= 1;
2761 }
2762 matches!(
2763 &source[start..end],
2764 "await"
2765 | "case"
2766 | "delete"
2767 | "do"
2768 | "else"
2769 | "in"
2770 | "instanceof"
2771 | "of"
2772 | "return"
2773 | "throw"
2774 | "typeof"
2775 | "void"
2776 | "yield"
2777 )
2778 }
2779 _ => false,
2780 };
2781 }
2782 true
2783}
2784
2785fn has_code_pattern_boundary(source: &str, index: usize, pattern: &str) -> bool {
2786 let bytes = source.as_bytes();
2787 let before_ok = index == 0
2788 || bytes
2789 .get(index - 1)
2790 .is_none_or(|byte| !is_js_ident_continue(*byte) && *byte != b'.');
2791 let end = index + pattern.len();
2792 let after_ok = pattern.ends_with('.')
2793 || bytes
2794 .get(end)
2795 .is_none_or(|byte| !is_js_ident_continue(*byte));
2796 before_ok && after_ok
2797}
2798
2799fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
2800 while source
2801 .as_bytes()
2802 .get(index)
2803 .is_some_and(u8::is_ascii_whitespace)
2804 {
2805 index += 1;
2806 }
2807 index
2808}
2809
2810fn collect_object_literal_keys(
2811 source: &str,
2812 open_brace: usize,
2813 names: &mut std::collections::HashSet<String>,
2814) -> usize {
2815 let mut depth = 0usize;
2816 let mut state = CjsScanState::Code;
2817 let mut entry_start = open_brace + 1;
2818 let bytes = source.as_bytes();
2819 let mut iter = source[open_brace..].char_indices().peekable();
2820 while let Some((offset, ch)) = iter.next() {
2821 let index = open_brace + offset;
2822 let byte = bytes[index];
2823 let next = bytes.get(index + 1).copied();
2824
2825 match state {
2826 CjsScanState::Code => {
2827 if byte == b'/' && next == Some(b'/') {
2828 state = CjsScanState::LineComment;
2829 continue;
2830 }
2831 if byte == b'/' && next == Some(b'*') {
2832 state = CjsScanState::BlockComment;
2833 continue;
2834 }
2835 if byte == b'\'' {
2836 state = CjsScanState::SingleQuote;
2837 continue;
2838 }
2839 if byte == b'"' {
2840 state = CjsScanState::DoubleQuote;
2841 continue;
2842 }
2843 if byte == b'`' {
2844 state = CjsScanState::Template;
2845 continue;
2846 }
2847 if byte == b'/' && slash_starts_regex_literal(source, index) {
2848 state = CjsScanState::Regex;
2849 continue;
2850 }
2851 match ch {
2852 '{' | '[' | '(' => depth += 1,
2853 '}' | ']' | ')' => {
2854 depth = depth.saturating_sub(1);
2855 if depth == 0 && ch == '}' {
2856 collect_object_literal_entry(&source[entry_start..index], names);
2857 return index + ch.len_utf8();
2858 }
2859 }
2860 ',' if depth == 1 => {
2861 collect_object_literal_entry(&source[entry_start..index], names);
2862 if names.len() >= MAX_CJS_NAMED_EXPORTS {
2863 return index + ch.len_utf8();
2864 }
2865 entry_start = index + ch.len_utf8();
2866 }
2867 _ => {}
2868 }
2869 }
2870 CjsScanState::LineComment => {
2871 if byte == b'\n' {
2872 state = CjsScanState::Code;
2873 }
2874 }
2875 CjsScanState::BlockComment => {
2876 if byte == b'*' && next == Some(b'/') {
2877 state = CjsScanState::Code;
2878 iter.next();
2879 }
2880 }
2881 CjsScanState::SingleQuote => {
2882 if byte == b'\\' {
2883 iter.next();
2884 } else if byte == b'\'' {
2885 state = CjsScanState::Code;
2886 }
2887 }
2888 CjsScanState::DoubleQuote => {
2889 if byte == b'\\' {
2890 iter.next();
2891 } else if byte == b'"' {
2892 state = CjsScanState::Code;
2893 }
2894 }
2895 CjsScanState::Template => {
2896 if byte == b'\\' {
2897 iter.next();
2898 } else if byte == b'`' {
2899 state = CjsScanState::Code;
2900 }
2901 }
2902 CjsScanState::Regex => {
2903 if byte == b'\\' {
2904 iter.next();
2905 } else if byte == b'[' {
2906 state = CjsScanState::RegexClass;
2907 } else if byte == b'/' {
2908 state = CjsScanState::Code;
2909 }
2910 }
2911 CjsScanState::RegexClass => {
2912 if byte == b'\\' {
2913 iter.next();
2914 } else if byte == b']' {
2915 state = CjsScanState::Regex;
2916 }
2917 }
2918 }
2919 }
2920 source.len()
2921}
2922
2923fn collect_object_literal_entry(entry: &str, names: &mut std::collections::HashSet<String>) {
2924 let key = entry_key(entry);
2925 if is_valid_js_ident(key) && key != "default" && key != "__esModule" {
2926 names.insert(key.to_string());
2927 }
2928}
2929
2930fn entry_key(entry: &str) -> &str {
2931 let trimmed = entry.trim();
2932 if let Some((quoted, end)) = parse_quoted_string_literal(trimmed, 0) {
2933 let next = skip_ascii_whitespace(trimmed, end);
2934 if trimmed.as_bytes().get(next) == Some(&b':') {
2935 return quoted;
2936 }
2937 return "";
2938 }
2939 trimmed
2940 .find(':')
2941 .map(|separator| &trimmed[..separator])
2942 .unwrap_or(trimmed)
2943 .trim()
2944}
2945
2946fn parse_quoted_string_literal(source: &str, index: usize) -> Option<(&str, usize)> {
2947 let quote = *source.as_bytes().get(index)?;
2948 if quote != b'\'' && quote != b'"' {
2949 return None;
2950 }
2951 let mut cursor = index + 1;
2952 while cursor < source.len() {
2953 let byte = source.as_bytes()[cursor];
2954 if byte == b'\\' {
2955 cursor = cursor.saturating_add(2);
2956 continue;
2957 }
2958 if byte == quote {
2959 let value = &source[index + 1..cursor];
2960 return Some((value, cursor + 1));
2961 }
2962 cursor += 1;
2963 }
2964 None
2965}
2966
2967fn source_has_dynamic_cjs_reexports(source: &str) -> bool {
2973 source.contains("__exportStar")
2974 || source.contains("Object.assign(exports")
2975 || source.contains("Object.assign(module.exports")
2976 || source.contains("Object.defineProperties(exports")
2977 || source.contains("Object.defineProperties(module.exports")
2978 || source_has_module_exports_object_spread(source)
2979}
2980
2981fn source_has_module_exports_object_spread(source: &str) -> bool {
2982 let mut cursor = 0usize;
2983 while let Some(start) = find_code_pattern(source, "module.exports", cursor) {
2984 let mut index = skip_ascii_whitespace(source, start + "module.exports".len());
2985 if source.as_bytes().get(index) != Some(&b'=') {
2986 cursor = start + "module.exports".len();
2987 continue;
2988 }
2989 index = skip_ascii_whitespace(source, index + 1);
2990 if source.as_bytes().get(index) != Some(&b'{') {
2991 cursor = index.saturating_add(1);
2992 continue;
2993 }
2994 let end = collect_object_literal_keys(source, index, &mut HashSet::new());
2995 if source[index..end.min(source.len())].contains("...") {
2996 return true;
2997 }
2998 cursor = end;
2999 }
3000 false
3001}
3002
3003fn add_esm_runtime_prelude(source: &str) -> String {
3004 let mut prelude = String::new();
3005
3006 if source.contains("require(")
3007 && !source.contains("createRequire(import.meta.url)")
3008 && !source.contains("createRequire(")
3009 && !source.contains("const require =")
3010 && !source.contains("let require =")
3011 && !source.contains("var require =")
3012 && !source.contains("function require(")
3013 {
3014 prelude
3015 .push_str("const require = globalThis._moduleModule.createRequire(import.meta.url);\n");
3016 }
3017
3018 for (name, triggers) in [
3019 ("fetch", &["fetch("][..]),
3020 ("Headers", &["Headers", "new Headers("][..]),
3021 ("Request", &["Request", "new Request("][..]),
3022 ("Response", &["Response", "new Response("][..]),
3023 ("Blob", &["Blob", "new Blob("][..]),
3024 ("File", &["File", "new File("][..]),
3025 ("FormData", &["FormData", "new FormData("][..]),
3026 ] {
3027 if needs_esm_global_alias(source, name, triggers) {
3028 prelude.push_str(&format!("const {name} = globalThis.{name};\n"));
3029 }
3030 }
3031
3032 if prelude.is_empty() {
3033 source.to_owned()
3034 } else {
3035 format!("{prelude}{source}")
3036 }
3037}
3038
3039fn needs_esm_global_alias(source: &str, name: &str, triggers: &[&str]) -> bool {
3040 if !triggers.iter().any(|trigger| source.contains(trigger)) {
3041 return false;
3042 }
3043
3044 if has_named_import_binding(source, name) {
3045 return false;
3046 }
3047
3048 for pattern in [
3049 format!("const {name}"),
3050 format!("let {name}"),
3051 format!("var {name}"),
3052 format!("function {name}"),
3053 format!("class {name}"),
3054 format!("import {name} from"),
3055 format!("import * as {name}"),
3056 ] {
3057 if source.contains(&pattern) {
3058 return false;
3059 }
3060 }
3061
3062 true
3063}
3064
3065fn has_named_import_binding(source: &str, name: &str) -> bool {
3066 #[derive(Clone, Copy, PartialEq, Eq)]
3067 enum ScanState {
3068 Code,
3069 LineComment,
3070 BlockComment,
3071 SingleQuote,
3072 DoubleQuote,
3073 Template,
3074 }
3075
3076 let bytes = source.as_bytes();
3077 let mut state = ScanState::Code;
3078 let mut index = 0usize;
3079
3080 while index < bytes.len() {
3081 let byte = bytes[index];
3082 let next = bytes.get(index + 1).copied();
3083
3084 match state {
3085 ScanState::Code => {
3086 if byte == b'/' && next == Some(b'/') {
3087 state = ScanState::LineComment;
3088 index += 2;
3089 continue;
3090 }
3091 if byte == b'/' && next == Some(b'*') {
3092 state = ScanState::BlockComment;
3093 index += 2;
3094 continue;
3095 }
3096 if byte == b'\'' {
3097 state = ScanState::SingleQuote;
3098 index += 1;
3099 continue;
3100 }
3101 if byte == b'"' {
3102 state = ScanState::DoubleQuote;
3103 index += 1;
3104 continue;
3105 }
3106 if byte == b'`' {
3107 state = ScanState::Template;
3108 index += 1;
3109 continue;
3110 }
3111 if !is_js_ident_start(byte) {
3112 index += 1;
3113 continue;
3114 }
3115
3116 let start = index;
3117 index += 1;
3118 while index < bytes.len() && is_js_ident_continue(bytes[index]) {
3119 index += 1;
3120 }
3121 if &source[start..index] != "import" {
3122 continue;
3123 }
3124
3125 let mut cursor = index;
3126 while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
3127 cursor += 1;
3128 }
3129 if bytes.get(cursor).copied() != Some(b'{') {
3130 continue;
3131 }
3132 cursor += 1;
3133 let imports_start = cursor;
3134 while cursor < bytes.len() && bytes[cursor] != b'}' {
3135 cursor += 1;
3136 }
3137 if cursor >= bytes.len() {
3138 return false;
3139 }
3140 if named_imports_bind_name(&source[imports_start..cursor], name) {
3141 return true;
3142 }
3143 index = cursor + 1;
3144 }
3145 ScanState::LineComment => {
3146 if byte == b'\n' {
3147 state = ScanState::Code;
3148 }
3149 index += 1;
3150 }
3151 ScanState::BlockComment => {
3152 if byte == b'*' && next == Some(b'/') {
3153 state = ScanState::Code;
3154 index += 2;
3155 } else {
3156 index += 1;
3157 }
3158 }
3159 ScanState::SingleQuote => {
3160 if byte == b'\\' {
3161 index += 2;
3162 } else if byte == b'\'' {
3163 state = ScanState::Code;
3164 index += 1;
3165 } else {
3166 index += 1;
3167 }
3168 }
3169 ScanState::DoubleQuote => {
3170 if byte == b'\\' {
3171 index += 2;
3172 } else if byte == b'"' {
3173 state = ScanState::Code;
3174 index += 1;
3175 } else {
3176 index += 1;
3177 }
3178 }
3179 ScanState::Template => {
3180 if byte == b'\\' {
3181 index += 2;
3182 } else if byte == b'`' {
3183 state = ScanState::Code;
3184 index += 1;
3185 } else {
3186 index += 1;
3187 }
3188 }
3189 }
3190 }
3191 false
3192}
3193
3194fn named_imports_bind_name(imports: &str, name: &str) -> bool {
3195 imports.split(',').any(|part| {
3196 let local = part
3197 .split_once(" as ")
3198 .map(|(_, alias)| alias)
3199 .unwrap_or(part);
3200 local.trim() == name
3201 })
3202}
3203
3204fn is_valid_js_ident(s: &str) -> bool {
3205 if s.is_empty() {
3206 return false;
3207 }
3208 if is_js_reserved_word(s) {
3209 return false;
3210 }
3211 let mut chars = s.chars();
3212 let first = chars.next().unwrap();
3213 if !first.is_alphabetic() && first != '_' && first != '$' {
3214 return false;
3215 }
3216 chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
3217}
3218
3219fn is_js_reserved_word(s: &str) -> bool {
3220 matches!(
3221 s,
3222 "arguments"
3223 | "as"
3224 | "async"
3225 | "await"
3226 | "break"
3227 | "case"
3228 | "catch"
3229 | "class"
3230 | "const"
3231 | "continue"
3232 | "debugger"
3233 | "default"
3234 | "delete"
3235 | "do"
3236 | "else"
3237 | "enum"
3238 | "eval"
3239 | "export"
3240 | "extends"
3241 | "false"
3242 | "finally"
3243 | "for"
3244 | "from"
3245 | "function"
3246 | "get"
3247 | "if"
3248 | "implements"
3249 | "import"
3250 | "in"
3251 | "instanceof"
3252 | "interface"
3253 | "let"
3254 | "new"
3255 | "null"
3256 | "of"
3257 | "package"
3258 | "private"
3259 | "protected"
3260 | "public"
3261 | "return"
3262 | "set"
3263 | "static"
3264 | "super"
3265 | "switch"
3266 | "target"
3267 | "this"
3268 | "throw"
3269 | "true"
3270 | "try"
3271 | "typeof"
3272 | "var"
3273 | "void"
3274 | "while"
3275 | "with"
3276 | "yield"
3277 )
3278}
3279
3280#[cfg(test)]
3281mod tests {
3282 use super::*;
3283 use crate::bridge;
3284 use crate::host_call::BridgeCallContext;
3285 use crate::isolate;
3286 use std::collections::HashMap;
3287 use std::io::{Cursor, Write};
3288 use std::sync::{Arc, Mutex};
3289
3290 #[test]
3291 fn strip_leading_shebang_matches_node() {
3292 assert_eq!(
3294 strip_leading_shebang("#!/usr/bin/env node\nexport const x = 1;\n"),
3295 "\nexport const x = 1;\n"
3296 );
3297 assert_eq!(
3299 strip_leading_shebang("export const x = 1;\n"),
3300 "export const x = 1;\n"
3301 );
3302 assert_eq!(strip_leading_shebang(" #!nope\n"), " #!nope\n");
3304 assert_eq!(strip_leading_shebang("#!/usr/bin/env node"), "");
3306 }
3307
3308 struct SharedWriter(Arc<Mutex<Vec<u8>>>);
3310
3311 impl Write for SharedWriter {
3312 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3313 self.0.lock().unwrap().write(buf)
3314 }
3315 fn flush(&mut self) -> std::io::Result<()> {
3316 self.0.lock().unwrap().flush()
3317 }
3318 }
3319
3320 #[test]
3321 fn esm_global_alias_detection_handles_multiline_named_imports() {
3322 let source = r#"
3323import {
3324 Blob,
3325 File,
3326 FormData
3327} from "fetch-blob/from.js";
3328
3329export { File };
3330"#;
3331
3332 assert!(!needs_esm_global_alias(source, "File", &["File"]));
3333 }
3334
3335 #[test]
3336 fn esm_global_alias_detection_handles_named_import_aliases() {
3337 let source = r#"
3338import {
3339 File as RuntimeFile
3340} from "fetch-blob/from.js";
3341
3342export const file = RuntimeFile;
3343"#;
3344
3345 assert!(!needs_esm_global_alias(
3346 source,
3347 "RuntimeFile",
3348 &["RuntimeFile"]
3349 ));
3350 }
3351
3352 #[test]
3353 fn esm_global_alias_detection_ignores_commented_named_imports() {
3354 let source = r#"
3355// import { File } from "fetch-blob/from.js";
3356/*
3357import {
3358 Blob,
3359 File
3360} from "fetch-blob/from.js";
3361*/
3362export function makeFile() {
3363 return new File([], "empty.txt");
3364}
3365"#;
3366
3367 assert!(needs_esm_global_alias(source, "File", &["new File("]));
3368 }
3369
3370 #[test]
3371 fn esm_global_alias_detection_ignores_string_named_imports() {
3372 let source = r#"
3373const example = "import { File } from 'fetch-blob/from.js'";
3374const singleQuoteExample = 'import { File } from "fetch-blob/from.js"';
3375const template = `import {
3376 File
3377} from "fetch-blob/from.js"`;
3378
3379export const file = new File([], "empty.txt");
3380"#;
3381
3382 assert!(needs_esm_global_alias(source, "File", &["new File("]));
3383 }
3384
3385 fn v8_serialize_str(
3387 iso: &mut v8::OwnedIsolate,
3388 ctx: &v8::Global<v8::Context>,
3389 s: &str,
3390 ) -> Vec<u8> {
3391 let scope = &mut v8::HandleScope::new(iso);
3392 let local = v8::Local::new(scope, ctx);
3393 let scope = &mut v8::ContextScope::new(scope, local);
3394 let val = v8::String::new(scope, s).unwrap();
3395 crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3396 }
3397
3398 fn v8_serialize_int(
3400 iso: &mut v8::OwnedIsolate,
3401 ctx: &v8::Global<v8::Context>,
3402 n: i64,
3403 ) -> Vec<u8> {
3404 let scope = &mut v8::HandleScope::new(iso);
3405 let local = v8::Local::new(scope, ctx);
3406 let scope = &mut v8::ContextScope::new(scope, local);
3407 let val = v8::Number::new(scope, n as f64);
3408 crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3409 }
3410
3411 fn v8_serialize_null(iso: &mut v8::OwnedIsolate, ctx: &v8::Global<v8::Context>) -> Vec<u8> {
3413 let scope = &mut v8::HandleScope::new(iso);
3414 let local = v8::Local::new(scope, ctx);
3415 let scope = &mut v8::ContextScope::new(scope, local);
3416 let val = v8::null(scope);
3417 crate::bridge::serialize_v8_value(scope, val.into()).unwrap()
3418 }
3419
3420 fn v8_serialize_eval(
3422 iso: &mut v8::OwnedIsolate,
3423 ctx: &v8::Global<v8::Context>,
3424 expr: &str,
3425 ) -> Vec<u8> {
3426 let scope = &mut v8::HandleScope::new(iso);
3427 let local = v8::Local::new(scope, ctx);
3428 let scope = &mut v8::ContextScope::new(scope, local);
3429 let source = v8::String::new(scope, expr).unwrap();
3430 let script = v8::Script::compile(scope, source, None).unwrap();
3431 let val = script.run(scope).unwrap();
3432 crate::bridge::serialize_v8_value(scope, val).unwrap()
3433 }
3434
3435 fn eval(
3437 isolate: &mut v8::OwnedIsolate,
3438 context: &v8::Global<v8::Context>,
3439 code: &str,
3440 ) -> String {
3441 let scope = &mut v8::HandleScope::new(isolate);
3442 let local = v8::Local::new(scope, context);
3443 let scope = &mut v8::ContextScope::new(scope, local);
3444 let source = v8::String::new(scope, code).unwrap();
3445 let script = v8::Script::compile(scope, source, None).unwrap();
3446 let result = script.run(scope).unwrap();
3447 result.to_rust_string_lossy(scope)
3448 }
3449
3450 fn eval_bool(
3452 isolate: &mut v8::OwnedIsolate,
3453 context: &v8::Global<v8::Context>,
3454 code: &str,
3455 ) -> bool {
3456 let scope = &mut v8::HandleScope::new(isolate);
3457 let local = v8::Local::new(scope, context);
3458 let scope = &mut v8::ContextScope::new(scope, local);
3459 let source = v8::String::new(scope, code).unwrap();
3460 let script = v8::Script::compile(scope, source, None).unwrap();
3461 let result = script.run(scope).unwrap();
3462 result.boolean_value(scope)
3463 }
3464
3465 fn eval_throws(
3467 isolate: &mut v8::OwnedIsolate,
3468 context: &v8::Global<v8::Context>,
3469 code: &str,
3470 ) -> bool {
3471 let scope = &mut v8::HandleScope::new(isolate);
3472 let local = v8::Local::new(scope, context);
3473 let scope = &mut v8::ContextScope::new(scope, local);
3474 let tc = &mut v8::TryCatch::new(scope);
3475 let source = v8::String::new(tc, code).unwrap();
3476 if let Some(script) = v8::Script::compile(tc, source, None) {
3477 script.run(tc);
3478 }
3479 tc.has_caught()
3480 }
3481
3482 #[test]
3483 fn v8_consolidated_tests() {
3484 isolate::init_v8_platform();
3485
3486 for i in 0..3 {
3489 let mut isolate = isolate::create_isolate(None);
3490 let context = isolate::create_context(&mut isolate);
3491 let result = eval(&mut isolate, &context, &format!("{} + 1", i));
3492 assert_eq!(result, format!("{}", i + 1));
3493 }
3494 {
3496 let mut isolate = isolate::create_isolate(Some(16));
3497 let context = isolate::create_context(&mut isolate);
3498 assert_eq!(eval(&mut isolate, &context, "1 + 2"), "3");
3499 }
3500 {
3502 let mut isolate = isolate::create_isolate(None);
3503 let context = isolate::create_context(&mut isolate);
3504 assert_eq!(
3505 eval(&mut isolate, &context, "'hello' + ' world'"),
3506 "hello world"
3507 );
3508 }
3509 {
3511 let mut isolate = isolate::create_isolate(None);
3512 let context = isolate::create_context(&mut isolate);
3513 eval(&mut isolate, &context, "var x = 42;");
3514 assert_eq!(eval(&mut isolate, &context, "x"), "42");
3515 }
3516 {
3518 let mut isolate = isolate::create_isolate(None);
3519 let context = isolate::create_context(&mut isolate);
3520 let (code, error) = {
3521 let scope = &mut v8::HandleScope::new(&mut isolate);
3522 let ctx = v8::Local::new(scope, &context);
3523 let scope = &mut v8::ContextScope::new(scope, ctx);
3524 execute_script(
3525 scope,
3526 "",
3527 "for (let i = 0; i < 1100; i++) Promise.reject(new Error('boom ' + i));",
3528 &mut None,
3529 )
3530 };
3531 assert_eq!(code, 1);
3532 let error = error.expect("unhandled rejection limit error");
3533 assert_eq!(
3534 error.code.as_deref(),
3535 Some("ERR_AGENTOS_UNHANDLED_REJECTION_LIMIT")
3536 );
3537 assert!(error
3538 .message
3539 .contains("unhandled promise rejection registry exceeded limit"));
3540 }
3541 {
3543 let mut isolate = isolate::create_isolate(None);
3544 let context = isolate::create_context(&mut isolate);
3545 let (code, error) = {
3546 let scope = &mut v8::HandleScope::new(&mut isolate);
3547 let ctx = v8::Local::new(scope, &context);
3548 let scope = &mut v8::ContextScope::new(scope, ctx);
3549 execute_script(
3550 scope,
3551 "",
3552 r#"
3553 const promises = [];
3554 for (let i = 0; i < 1100; i++) promises.push(Promise.reject(new Error('boom ' + i)));
3555 for (const promise of promises) promise.catch(() => {});
3556 "#,
3557 &mut None,
3558 )
3559 };
3560 assert_eq!(code, 0);
3561 assert!(
3562 error.is_none(),
3563 "handled over-cap rejections should not surface a limit error"
3564 );
3565 }
3566
3567 {
3569 let mut isolate = isolate::create_isolate(None);
3570 let context = isolate::create_context(&mut isolate);
3571
3572 let mut env = HashMap::new();
3573 env.insert("HOME".into(), "/home/agentos".into());
3574 env.insert("PATH".into(), "/usr/bin".into());
3575
3576 let process_config = ProcessConfig {
3577 cwd: "/app".into(),
3578 env,
3579 timing_mitigation: "none".into(),
3580 frozen_time_ms: Some(1700000000000.0),
3581 high_resolution_time: true,
3582 };
3583 let os_config = OsConfig {
3584 homedir: "/home/agentos".into(),
3585 tmpdir: "/tmp".into(),
3586 platform: "linux".into(),
3587 arch: "x64".into(),
3588 };
3589
3590 {
3592 let scope = &mut v8::HandleScope::new(&mut isolate);
3593 let ctx = v8::Local::new(scope, &context);
3594 let scope = &mut v8::ContextScope::new(scope, ctx);
3595 inject_globals(scope, &process_config, &os_config);
3596 }
3597
3598 assert_eq!(eval(&mut isolate, &context, "_processConfig.cwd"), "/app");
3600 assert_eq!(
3601 eval(&mut isolate, &context, "_processConfig.timing_mitigation"),
3602 "none"
3603 );
3604 assert_eq!(
3605 eval(&mut isolate, &context, "_processConfig.frozen_time_ms"),
3606 "1700000000000"
3607 );
3608 assert_eq!(
3609 eval(
3610 &mut isolate,
3611 &context,
3612 "_processConfig.high_resolution_time"
3613 ),
3614 "true"
3615 );
3616 assert_eq!(
3617 eval(&mut isolate, &context, "_processConfig.env.HOME"),
3618 "/home/agentos"
3619 );
3620 assert_eq!(
3621 eval(&mut isolate, &context, "_processConfig.env.PATH"),
3622 "/usr/bin"
3623 );
3624
3625 assert_eq!(
3627 eval(&mut isolate, &context, "_osConfig.homedir"),
3628 "/home/agentos"
3629 );
3630 assert_eq!(eval(&mut isolate, &context, "_osConfig.tmpdir"), "/tmp");
3631 assert_eq!(eval(&mut isolate, &context, "_osConfig.platform"), "linux");
3632 assert_eq!(eval(&mut isolate, &context, "_osConfig.arch"), "x64");
3633 }
3634
3635 {
3637 let mut isolate = isolate::create_isolate(None);
3638 let context = isolate::create_context(&mut isolate);
3639 let payload = v8_serialize_eval(
3640 &mut isolate,
3641 &context,
3642 r#"({
3643 processConfig: {
3644 cwd: "/app",
3645 env: { HOME: "/home/agentos" },
3646 timing_mitigation: "none",
3647 frozen_time_ms: null
3648 }
3649 })"#,
3650 );
3651
3652 let err = {
3653 let scope = &mut v8::HandleScope::new(&mut isolate);
3654 let ctx = v8::Local::new(scope, &context);
3655 let scope = &mut v8::ContextScope::new(scope, ctx);
3656 inject_globals_from_payload(scope, &payload).expect_err("missing osConfig")
3657 };
3658
3659 assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3660 assert!(
3661 err.message.contains("missing osConfig"),
3662 "unexpected error message: {}",
3663 err.message
3664 );
3665 assert_eq!(
3666 eval(&mut isolate, &context, "typeof _processConfig"),
3667 "undefined",
3668 "invalid payload must not partially inject process config"
3669 );
3670 assert_eq!(
3671 eval(&mut isolate, &context, "typeof _osConfig"),
3672 "undefined",
3673 "invalid payload must not inject os config"
3674 );
3675 }
3676
3677 {
3679 let mut isolate = isolate::create_isolate(None);
3680 let context = isolate::create_context(&mut isolate);
3681 let payload = v8_serialize_eval(
3682 &mut isolate,
3683 &context,
3684 r#"({
3685 processConfig: "not-an-object",
3686 osConfig: {
3687 homedir: "/home/agentos",
3688 tmpdir: "/tmp",
3689 platform: "linux",
3690 arch: "x64"
3691 }
3692 })"#,
3693 );
3694
3695 let err = {
3696 let scope = &mut v8::HandleScope::new(&mut isolate);
3697 let ctx = v8::Local::new(scope, &context);
3698 let scope = &mut v8::ContextScope::new(scope, ctx);
3699 inject_globals_from_payload(scope, &payload).expect_err("primitive processConfig")
3700 };
3701
3702 assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3703 assert!(
3704 err.message.contains("processConfig is not an object"),
3705 "unexpected error message: {}",
3706 err.message
3707 );
3708 assert_eq!(
3709 eval(&mut isolate, &context, "typeof _processConfig"),
3710 "undefined",
3711 "wrong-type payload must not inject primitive process config"
3712 );
3713 }
3714
3715 {
3717 let mut isolate = isolate::create_isolate(None);
3718 let context = isolate::create_context(&mut isolate);
3719 let payload = v8_serialize_eval(
3720 &mut isolate,
3721 &context,
3722 r#"({
3723 processConfig: {
3724 cwd: "/app",
3725 env: "not-an-object",
3726 timing_mitigation: "none",
3727 frozen_time_ms: null
3728 },
3729 osConfig: {
3730 homedir: "/home/agentos",
3731 tmpdir: "/tmp",
3732 platform: "linux",
3733 arch: "x64"
3734 }
3735 })"#,
3736 );
3737
3738 let err = {
3739 let scope = &mut v8::HandleScope::new(&mut isolate);
3740 let ctx = v8::Local::new(scope, &context);
3741 let scope = &mut v8::ContextScope::new(scope, ctx);
3742 inject_globals_from_payload(scope, &payload).expect_err("primitive env")
3743 };
3744
3745 assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3746 assert!(
3747 err.message.contains("processConfig.env is not an object"),
3748 "unexpected error message: {}",
3749 err.message
3750 );
3751 assert_eq!(
3752 eval(&mut isolate, &context, "typeof _processConfig"),
3753 "undefined",
3754 "wrong-type env payload must not partially inject process config"
3755 );
3756 }
3757
3758 {
3760 let mut isolate = isolate::create_isolate(None);
3761 let context = isolate::create_context(&mut isolate);
3762 let payload = v8_serialize_eval(
3763 &mut isolate,
3764 &context,
3765 r#"({
3766 processConfig: {
3767 cwd: "/app",
3768 timing_mitigation: "none",
3769 frozen_time_ms: null
3770 },
3771 osConfig: {
3772 homedir: "/home/agentos",
3773 tmpdir: "/tmp",
3774 platform: "linux",
3775 arch: "x64"
3776 }
3777 })"#,
3778 );
3779
3780 let err = {
3781 let scope = &mut v8::HandleScope::new(&mut isolate);
3782 let ctx = v8::Local::new(scope, &context);
3783 let scope = &mut v8::ContextScope::new(scope, ctx);
3784 inject_globals_from_payload(scope, &payload).expect_err("missing env")
3785 };
3786
3787 assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3788 assert!(
3789 err.message.contains("missing processConfig.env"),
3790 "unexpected error message: {}",
3791 err.message
3792 );
3793 assert_eq!(
3794 eval(&mut isolate, &context, "typeof _processConfig"),
3795 "undefined",
3796 "missing env payload must not partially inject process config"
3797 );
3798 }
3799
3800 {
3802 let mut isolate = isolate::create_isolate(None);
3803 let context = isolate::create_context(&mut isolate);
3804 let payload = v8_serialize_eval(
3805 &mut isolate,
3806 &context,
3807 r#"({
3808 processConfig: {
3809 cwd: "/app",
3810 env: new Uint8Array([1]),
3811 timing_mitigation: "none",
3812 frozen_time_ms: null
3813 },
3814 osConfig: {
3815 homedir: "/home/agentos",
3816 tmpdir: "/tmp",
3817 platform: "linux",
3818 arch: "x64"
3819 }
3820 })"#,
3821 );
3822
3823 let err = {
3824 let scope = &mut v8::HandleScope::new(&mut isolate);
3825 let ctx = v8::Local::new(scope, &context);
3826 let scope = &mut v8::ContextScope::new(scope, ctx);
3827 inject_globals_from_payload(scope, &payload).expect_err("typed array env")
3828 };
3829
3830 assert_eq!(err.code.as_deref(), Some("ERR_INVALID_GLOBALS_PAYLOAD"));
3831 assert!(
3832 err.message
3833 .contains("processConfig.env is not a plain object"),
3834 "unexpected error message: {}",
3835 err.message
3836 );
3837 assert_eq!(
3838 eval(&mut isolate, &context, "typeof _processConfig"),
3839 "undefined",
3840 "typed-array env payload must not partially inject process config"
3841 );
3842 }
3843
3844 {
3846 let mut isolate = isolate::create_isolate(None);
3847 let context = isolate::create_context(&mut isolate);
3848 let payload = v8_serialize_eval(
3849 &mut isolate,
3850 &context,
3851 r#"({
3852 processConfig: {
3853 cwd: "/app",
3854 env: { HOME: "/home/agentos" },
3855 timing_mitigation: "none",
3856 frozen_time_ms: null
3857 },
3858 osConfig: {
3859 homedir: "/home/agentos",
3860 tmpdir: "/tmp",
3861 platform: "linux",
3862 arch: "x64"
3863 }
3864 })"#,
3865 );
3866
3867 {
3868 let scope = &mut v8::HandleScope::new(&mut isolate);
3869 let ctx = v8::Local::new(scope, &context);
3870 let scope = &mut v8::ContextScope::new(scope, ctx);
3871 inject_globals_from_payload(scope, &payload).expect("valid globals payload");
3872 }
3873
3874 assert_eq!(eval(&mut isolate, &context, "_processConfig.cwd"), "/app");
3875 assert_eq!(
3876 eval(&mut isolate, &context, "_processConfig.env.HOME"),
3877 "/home/agentos"
3878 );
3879 assert!(eval_bool(
3880 &mut isolate,
3881 &context,
3882 "Object.isFrozen(_processConfig) && Object.isFrozen(_processConfig.env) && Object.isFrozen(_osConfig)"
3883 ));
3884 }
3885
3886 {
3888 let mut isolate = isolate::create_isolate(None);
3889 let context = isolate::create_context(&mut isolate);
3890
3891 let process_config = ProcessConfig {
3892 cwd: "/".into(),
3893 env: HashMap::new(),
3894 timing_mitigation: "none".into(),
3895 frozen_time_ms: None,
3896 high_resolution_time: false,
3897 };
3898 let os_config = OsConfig {
3899 homedir: "/root".into(),
3900 tmpdir: "/tmp".into(),
3901 platform: "linux".into(),
3902 arch: "x64".into(),
3903 };
3904
3905 {
3906 let scope = &mut v8::HandleScope::new(&mut isolate);
3907 let ctx = v8::Local::new(scope, &context);
3908 let scope = &mut v8::ContextScope::new(scope, ctx);
3909 inject_globals(scope, &process_config, &os_config);
3910 }
3911
3912 assert_eq!(
3913 eval(
3914 &mut isolate,
3915 &context,
3916 "_processConfig.frozen_time_ms === null"
3917 ),
3918 "true"
3919 );
3920 }
3921
3922 {
3924 let mut isolate = isolate::create_isolate(None);
3925 let context = isolate::create_context(&mut isolate);
3926
3927 let process_config = ProcessConfig {
3928 cwd: "/app".into(),
3929 env: HashMap::new(),
3930 timing_mitigation: "none".into(),
3931 frozen_time_ms: None,
3932 high_resolution_time: false,
3933 };
3934 let os_config = OsConfig {
3935 homedir: "/home".into(),
3936 tmpdir: "/tmp".into(),
3937 platform: "linux".into(),
3938 arch: "x64".into(),
3939 };
3940
3941 {
3942 let scope = &mut v8::HandleScope::new(&mut isolate);
3943 let ctx = v8::Local::new(scope, &context);
3944 let scope = &mut v8::ContextScope::new(scope, ctx);
3945 inject_globals(scope, &process_config, &os_config);
3946 }
3947
3948 assert!(eval_bool(
3950 &mut isolate,
3951 &context,
3952 "Object.isFrozen(_processConfig)"
3953 ));
3954 assert!(eval_bool(
3955 &mut isolate,
3956 &context,
3957 "Object.isFrozen(_osConfig)"
3958 ));
3959 assert!(eval_bool(
3960 &mut isolate,
3961 &context,
3962 "Object.isFrozen(_processConfig.env)"
3963 ));
3964
3965 assert!(eval_throws(
3967 &mut isolate,
3968 &context,
3969 "'use strict'; _processConfig.cwd = '/hacked'"
3970 ));
3971 assert!(eval_throws(
3972 &mut isolate,
3973 &context,
3974 "'use strict'; _osConfig.platform = 'hacked'"
3975 ));
3976
3977 assert!(eval_throws(
3979 &mut isolate,
3980 &context,
3981 "'use strict'; delete _processConfig"
3982 ));
3983 assert!(eval_throws(
3984 &mut isolate,
3985 &context,
3986 "Object.defineProperty(globalThis, '_processConfig', { value: {} })"
3987 ));
3988 assert!(eval_throws(
3989 &mut isolate,
3990 &context,
3991 "Object.defineProperty(globalThis, '_osConfig', { value: {} })"
3992 ));
3993 }
3994
3995 {
4001 let mut isolate = isolate::create_isolate(None);
4002 let context = isolate::create_context(&mut isolate);
4003
4004 let process_config = ProcessConfig {
4005 cwd: "/".into(),
4006 env: HashMap::new(),
4007 timing_mitigation: "freeze".into(),
4008 frozen_time_ms: None,
4009 high_resolution_time: false,
4010 };
4011 let os_config = OsConfig {
4012 homedir: "/root".into(),
4013 tmpdir: "/tmp".into(),
4014 platform: "linux".into(),
4015 arch: "x64".into(),
4016 };
4017
4018 {
4019 let scope = &mut v8::HandleScope::new(&mut isolate);
4020 let ctx = v8::Local::new(scope, &context);
4021 let scope = &mut v8::ContextScope::new(scope, ctx);
4022 inject_globals(scope, &process_config, &os_config);
4023 }
4024
4025 assert!(eval_bool(
4027 &mut isolate,
4028 &context,
4029 "typeof SharedArrayBuffer !== 'undefined'"
4030 ));
4031 assert_eq!(
4033 eval(&mut isolate, &context, "_processConfig.timing_mitigation"),
4034 "freeze"
4035 );
4036 }
4037
4038 {
4040 let mut isolate = isolate::create_isolate(None);
4041 let context = isolate::create_context(&mut isolate);
4042
4043 let process_config = ProcessConfig {
4044 cwd: "/".into(),
4045 env: HashMap::new(),
4046 timing_mitigation: "none".into(),
4047 frozen_time_ms: None,
4048 high_resolution_time: false,
4049 };
4050 let os_config = OsConfig {
4051 homedir: "/root".into(),
4052 tmpdir: "/tmp".into(),
4053 platform: "linux".into(),
4054 arch: "x64".into(),
4055 };
4056
4057 {
4058 let scope = &mut v8::HandleScope::new(&mut isolate);
4059 let ctx = v8::Local::new(scope, &context);
4060 let scope = &mut v8::ContextScope::new(scope, ctx);
4061 inject_globals(scope, &process_config, &os_config);
4062 }
4063
4064 assert!(eval_bool(
4066 &mut isolate,
4067 &context,
4068 "typeof SharedArrayBuffer !== 'undefined'"
4069 ));
4070 }
4071
4072 {
4074 let mut isolate = isolate::create_isolate(None);
4075 let context = isolate::create_context(&mut isolate);
4076
4077 assert!(!eval_throws(
4078 &mut isolate,
4079 &context,
4080 "new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0]))"
4081 ));
4082 }
4083
4084 {
4086 let mut isolate = isolate::create_isolate(None);
4087 let context = isolate::create_context(&mut isolate);
4088
4089 let result = eval(
4090 &mut isolate,
4091 &context,
4092 r#"
4093 (function() {
4094 var bytes = new Uint8Array([
4095 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
4096 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
4097 0x03, 0x02, 0x01, 0x00,
4098 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
4099 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
4100 ]);
4101 var module = new WebAssembly.Module(bytes);
4102 var instance = new WebAssembly.Instance(module, {});
4103 return String(instance.exports.add(19, 23));
4104 })()
4105 "#,
4106 );
4107 assert_eq!(result, "42");
4108 }
4109
4110 {
4112 let mut isolate = isolate::create_isolate(None);
4113 let context = isolate::create_context(&mut isolate);
4114
4115 let limit_report = eval(
4116 &mut isolate,
4117 &context,
4118 r#"
4119 (function() {
4120 function capture(fn) {
4121 try {
4122 fn();
4123 return "ALLOWED";
4124 } catch (error) {
4125 return error.name + ":" + error.message;
4126 }
4127 }
4128
4129 var moduleLimit = capture(function() {
4130 var bytes = new Uint8Array([
4131 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
4132 0x05, 0x06, 0x01, 0x01, 0x01, 0x81, 0x80, 0x04,
4133 ]);
4134 new WebAssembly.Module(bytes);
4135 });
4136 var memoryLimit = capture(function() {
4137 new WebAssembly.Memory({ initial: 1, maximum: 65537 });
4138 });
4139 return JSON.stringify({ moduleLimit: moduleLimit, memoryLimit: memoryLimit });
4140 })()
4141 "#,
4142 );
4143
4144 assert!(
4145 limit_report.contains(r#""moduleLimit":"CompileError:"#),
4146 "unexpected module limit report: {limit_report}"
4147 );
4148 assert!(
4149 limit_report.contains(r#""memoryLimit":"RangeError:"#),
4150 "unexpected memory limit report: {limit_report}"
4151 );
4152 assert!(
4153 limit_report.contains("65536"),
4154 "unexpected limit report: {limit_report}"
4155 );
4156 }
4157
4158 {
4160 let mut iso = isolate::create_isolate(None);
4161 let ctx = isolate::create_context(&mut iso);
4162
4163 let result_v8 = v8_serialize_str(&mut iso, &ctx, "hello world");
4165
4166 let mut response_buf = Vec::new();
4167 crate::ipc_binary::write_frame(
4168 &mut response_buf,
4169 &crate::ipc_binary::BinaryFrame::BridgeResponse {
4170 session_id: String::new(),
4171 call_id: 1,
4172 status: 0,
4173 payload: result_v8,
4174 },
4175 )
4176 .unwrap();
4177
4178 let bridge_ctx = BridgeCallContext::new(
4179 Box::new(Vec::new()),
4180 Box::new(Cursor::new(response_buf)),
4181 "test-session".into(),
4182 );
4183
4184 let _fn_store;
4185 {
4186 let scope = &mut v8::HandleScope::new(&mut iso);
4187 let local = v8::Local::new(scope, &ctx);
4188 let scope = &mut v8::ContextScope::new(scope, local);
4189 _fn_store = bridge::register_sync_bridge_fns(
4190 scope,
4191 &bridge_ctx as *const BridgeCallContext,
4192 &["_testBridge"],
4193 );
4194 }
4195
4196 assert_eq!(eval(&mut iso, &ctx, "_testBridge('arg1')"), "hello world");
4197 }
4198
4199 {
4201 let mut iso = isolate::create_isolate(None);
4202 let ctx = isolate::create_context(&mut iso);
4203
4204 let mut response_buf = Vec::new();
4205 crate::ipc_binary::write_frame(
4206 &mut response_buf,
4207 &crate::ipc_binary::BinaryFrame::BridgeResponse {
4208 session_id: String::new(),
4209 call_id: 1,
4210 status: 1,
4211 payload: "ENOENT: file not found".as_bytes().to_vec(),
4212 },
4213 )
4214 .unwrap();
4215
4216 let bridge_ctx = BridgeCallContext::new(
4217 Box::new(Vec::new()),
4218 Box::new(Cursor::new(response_buf)),
4219 "test-session".into(),
4220 );
4221
4222 let _fn_store;
4223 {
4224 let scope = &mut v8::HandleScope::new(&mut iso);
4225 let local = v8::Local::new(scope, &ctx);
4226 let scope = &mut v8::ContextScope::new(scope, local);
4227 _fn_store = bridge::register_sync_bridge_fns(
4228 scope,
4229 &bridge_ctx as *const BridgeCallContext,
4230 &["_testBridge"],
4231 );
4232 }
4233
4234 assert!(eval_throws(&mut iso, &ctx, "_testBridge('arg')"));
4235 }
4236
4237 {
4239 let mut iso = isolate::create_isolate(None);
4240 let ctx = isolate::create_context(&mut iso);
4241
4242 let r1_bytes = v8_serialize_str(&mut iso, &ctx, "result-one");
4244 let r2_bytes = v8_serialize_int(&mut iso, &ctx, 42);
4245
4246 let mut response_buf = Vec::new();
4247 crate::ipc_binary::write_frame(
4248 &mut response_buf,
4249 &crate::ipc_binary::BinaryFrame::BridgeResponse {
4250 session_id: String::new(),
4251 call_id: 1,
4252 status: 0,
4253 payload: r1_bytes,
4254 },
4255 )
4256 .unwrap();
4257 crate::ipc_binary::write_frame(
4258 &mut response_buf,
4259 &crate::ipc_binary::BinaryFrame::BridgeResponse {
4260 session_id: String::new(),
4261 call_id: 2,
4262 status: 0,
4263 payload: r2_bytes,
4264 },
4265 )
4266 .unwrap();
4267
4268 let bridge_ctx = BridgeCallContext::new(
4269 Box::new(Vec::new()),
4270 Box::new(Cursor::new(response_buf)),
4271 "test-session".into(),
4272 );
4273
4274 let _fn_store;
4275 {
4276 let scope = &mut v8::HandleScope::new(&mut iso);
4277 let local = v8::Local::new(scope, &ctx);
4278 let scope = &mut v8::ContextScope::new(scope, local);
4279 _fn_store = bridge::register_sync_bridge_fns(
4280 scope,
4281 &bridge_ctx as *const BridgeCallContext,
4282 &["_fn1", "_fn2"],
4283 );
4284 }
4285
4286 assert_eq!(eval(&mut iso, &ctx, "_fn1('x')"), "result-one");
4287 assert_eq!(eval(&mut iso, &ctx, "_fn2(1, 2, 3)"), "42");
4288 }
4289
4290 {
4292 let mut iso = isolate::create_isolate(None);
4293 let ctx = isolate::create_context(&mut iso);
4294
4295 let mut response_buf = Vec::new();
4296 crate::ipc_binary::write_frame(
4297 &mut response_buf,
4298 &crate::ipc_binary::BinaryFrame::BridgeResponse {
4299 session_id: String::new(),
4300 call_id: 1,
4301 status: 0,
4302 payload: vec![],
4303 },
4304 )
4305 .unwrap();
4306
4307 let bridge_ctx = BridgeCallContext::new(
4308 Box::new(Vec::new()),
4309 Box::new(Cursor::new(response_buf)),
4310 "test-session".into(),
4311 );
4312
4313 let _fn_store;
4314 {
4315 let scope = &mut v8::HandleScope::new(&mut iso);
4316 let local = v8::Local::new(scope, &ctx);
4317 let scope = &mut v8::ContextScope::new(scope, local);
4318 _fn_store = bridge::register_sync_bridge_fns(
4319 scope,
4320 &bridge_ctx as *const BridgeCallContext,
4321 &["_testBridge"],
4322 );
4323 }
4324
4325 assert!(eval_bool(&mut iso, &ctx, "_testBridge() === undefined"));
4326 }
4327
4328 {
4330 let mut iso = isolate::create_isolate(None);
4331 let ctx = isolate::create_context(&mut iso);
4332
4333 let writer_buf = Arc::new(Mutex::new(Vec::new()));
4334 let bridge_ctx = BridgeCallContext::new(
4335 Box::new(SharedWriter(Arc::clone(&writer_buf))),
4336 Box::new(Cursor::new(Vec::new())),
4337 "test-session".into(),
4338 );
4339 let pending = bridge::PendingPromises::new();
4340
4341 let _fn_store;
4342 {
4343 let scope = &mut v8::HandleScope::new(&mut iso);
4344 let local = v8::Local::new(scope, &ctx);
4345 let scope = &mut v8::ContextScope::new(scope, local);
4346 _fn_store = bridge::register_async_bridge_fns(
4347 scope,
4348 &bridge_ctx as *const BridgeCallContext,
4349 &pending as *const bridge::PendingPromises,
4350 &["_asyncFn"],
4351 );
4352 }
4353
4354 eval(&mut iso, &ctx, "var _promise = _asyncFn('arg1')");
4356
4357 {
4359 let written = writer_buf.lock().unwrap();
4360 let call = crate::ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
4361 match call {
4362 crate::ipc_binary::BinaryFrame::BridgeCall {
4363 call_id, method, ..
4364 } => {
4365 assert_eq!(call_id, 1);
4366 assert_eq!(method, "_asyncFn");
4367 }
4368 _ => panic!("expected BridgeCall"),
4369 }
4370 }
4371
4372 assert_eq!(pending.len(), 1);
4374 assert!(eval_bool(&mut iso, &ctx, "_promise instanceof Promise"));
4375
4376 let result_v8 = v8_serialize_str(&mut iso, &ctx, "async result");
4378
4379 {
4380 let scope = &mut v8::HandleScope::new(&mut iso);
4381 let local = v8::Local::new(scope, &ctx);
4382 let scope = &mut v8::ContextScope::new(scope, local);
4383 bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(result_v8), None)
4384 .unwrap();
4385 }
4386
4387 assert_eq!(pending.len(), 0);
4388
4389 {
4391 let scope = &mut v8::HandleScope::new(&mut iso);
4392 let local = v8::Local::new(scope, &ctx);
4393 let scope = &mut v8::ContextScope::new(scope, local);
4394 let source = v8::String::new(scope, "_promise").unwrap();
4395 let script = v8::Script::compile(scope, source, None).unwrap();
4396 let result = script.run(scope).unwrap();
4397 let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4398 assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4399 assert_eq!(
4400 promise.result(scope).to_rust_string_lossy(scope),
4401 "async result"
4402 );
4403 }
4404 }
4405
4406 {
4408 let mut iso = isolate::create_isolate(None);
4409 let ctx = isolate::create_context(&mut iso);
4410
4411 let bridge_ctx = BridgeCallContext::new(
4412 Box::new(Vec::new()),
4413 Box::new(Cursor::new(Vec::new())),
4414 "test-session".into(),
4415 );
4416 let pending = bridge::PendingPromises::new();
4417
4418 let _fn_store;
4419 {
4420 let scope = &mut v8::HandleScope::new(&mut iso);
4421 let local = v8::Local::new(scope, &ctx);
4422 let scope = &mut v8::ContextScope::new(scope, local);
4423 _fn_store = bridge::register_async_bridge_fns(
4424 scope,
4425 &bridge_ctx as *const BridgeCallContext,
4426 &pending as *const bridge::PendingPromises,
4427 &["_asyncFn"],
4428 );
4429 }
4430
4431 eval(&mut iso, &ctx, "var _promise = _asyncFn('arg')");
4432 assert_eq!(pending.len(), 1);
4433
4434 {
4436 let scope = &mut v8::HandleScope::new(&mut iso);
4437 let local = v8::Local::new(scope, &ctx);
4438 let scope = &mut v8::ContextScope::new(scope, local);
4439 bridge::resolve_pending_promise(
4440 scope,
4441 &pending,
4442 1,
4443 0,
4444 None,
4445 Some("ENOENT: file not found".into()),
4446 )
4447 .unwrap();
4448 }
4449
4450 assert_eq!(pending.len(), 0);
4451
4452 {
4454 let scope = &mut v8::HandleScope::new(&mut iso);
4455 let local = v8::Local::new(scope, &ctx);
4456 let scope = &mut v8::ContextScope::new(scope, local);
4457 let source = v8::String::new(scope, "_promise").unwrap();
4458 let script = v8::Script::compile(scope, source, None).unwrap();
4459 let result = script.run(scope).unwrap();
4460 let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4461 assert_eq!(promise.state(), v8::PromiseState::Rejected);
4462 let rejection = promise.result(scope);
4463 let obj = v8::Local::<v8::Object>::try_from(rejection).unwrap();
4464 let msg_key = v8::String::new(scope, "message").unwrap();
4465 let msg_val = obj.get(scope, msg_key.into()).unwrap();
4466 assert_eq!(
4467 msg_val.to_rust_string_lossy(scope),
4468 "ENOENT: file not found"
4469 );
4470 }
4471 }
4472
4473 {
4475 let mut iso = isolate::create_isolate(None);
4476 let ctx = isolate::create_context(&mut iso);
4477
4478 let bridge_ctx = BridgeCallContext::new(
4479 Box::new(Vec::new()),
4480 Box::new(Cursor::new(Vec::new())),
4481 "test-session".into(),
4482 );
4483 let pending = bridge::PendingPromises::new();
4484
4485 let _fn_store;
4486 {
4487 let scope = &mut v8::HandleScope::new(&mut iso);
4488 let local = v8::Local::new(scope, &ctx);
4489 let scope = &mut v8::ContextScope::new(scope, local);
4490 _fn_store = bridge::register_async_bridge_fns(
4491 scope,
4492 &bridge_ctx as *const BridgeCallContext,
4493 &pending as *const bridge::PendingPromises,
4494 &["_fetch", "_dns"],
4495 );
4496 }
4497
4498 eval(
4499 &mut iso,
4500 &ctx,
4501 "var _p1 = _fetch('url'); var _p2 = _dns('host')",
4502 );
4503 assert_eq!(pending.len(), 2);
4504
4505 let r2 = v8_serialize_str(&mut iso, &ctx, "dns-result");
4507 {
4508 let scope = &mut v8::HandleScope::new(&mut iso);
4509 let local = v8::Local::new(scope, &ctx);
4510 let scope = &mut v8::ContextScope::new(scope, local);
4511 bridge::resolve_pending_promise(scope, &pending, 2, 0, Some(r2), None).unwrap();
4512 }
4513 assert_eq!(pending.len(), 1);
4514
4515 let r1 = v8_serialize_str(&mut iso, &ctx, "fetch-result");
4516 {
4517 let scope = &mut v8::HandleScope::new(&mut iso);
4518 let local = v8::Local::new(scope, &ctx);
4519 let scope = &mut v8::ContextScope::new(scope, local);
4520 bridge::resolve_pending_promise(scope, &pending, 1, 0, Some(r1), None).unwrap();
4521 }
4522 assert_eq!(pending.len(), 0);
4523
4524 {
4526 let scope = &mut v8::HandleScope::new(&mut iso);
4527 let local = v8::Local::new(scope, &ctx);
4528 let scope = &mut v8::ContextScope::new(scope, local);
4529
4530 let source = v8::String::new(scope, "_p1").unwrap();
4531 let script = v8::Script::compile(scope, source, None).unwrap();
4532 let result = script.run(scope).unwrap();
4533 let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4534 assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4535 assert_eq!(
4536 promise.result(scope).to_rust_string_lossy(scope),
4537 "fetch-result"
4538 );
4539
4540 let source = v8::String::new(scope, "_p2").unwrap();
4541 let script = v8::Script::compile(scope, source, None).unwrap();
4542 let result = script.run(scope).unwrap();
4543 let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4544 assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4545 assert_eq!(
4546 promise.result(scope).to_rust_string_lossy(scope),
4547 "dns-result"
4548 );
4549 }
4550 }
4551
4552 {
4554 let mut iso = isolate::create_isolate(None);
4555 let ctx = isolate::create_context(&mut iso);
4556
4557 let bridge_ctx = BridgeCallContext::new(
4558 Box::new(Vec::new()),
4559 Box::new(Cursor::new(Vec::new())),
4560 "test-session".into(),
4561 );
4562 let pending = bridge::PendingPromises::new();
4563
4564 let _fn_store;
4565 {
4566 let scope = &mut v8::HandleScope::new(&mut iso);
4567 let local = v8::Local::new(scope, &ctx);
4568 let scope = &mut v8::ContextScope::new(scope, local);
4569 _fn_store = bridge::register_async_bridge_fns(
4570 scope,
4571 &bridge_ctx as *const BridgeCallContext,
4572 &pending as *const bridge::PendingPromises,
4573 &["_asyncFn"],
4574 );
4575 }
4576
4577 eval(&mut iso, &ctx, "var _promise = _asyncFn()");
4578
4579 {
4581 let scope = &mut v8::HandleScope::new(&mut iso);
4582 let local = v8::Local::new(scope, &ctx);
4583 let scope = &mut v8::ContextScope::new(scope, local);
4584 bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap();
4585 }
4586
4587 {
4589 let scope = &mut v8::HandleScope::new(&mut iso);
4590 let local = v8::Local::new(scope, &ctx);
4591 let scope = &mut v8::ContextScope::new(scope, local);
4592 let source = v8::String::new(scope, "_promise").unwrap();
4593 let script = v8::Script::compile(scope, source, None).unwrap();
4594 let result = script.run(scope).unwrap();
4595 let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
4596 assert_eq!(promise.state(), v8::PromiseState::Fulfilled);
4597 assert!(promise.result(scope).is_undefined());
4598 }
4599 }
4600
4601 {
4603 let mut iso = isolate::create_isolate(None);
4604 let ctx = isolate::create_context(&mut iso);
4605
4606 let bridge_ctx = BridgeCallContext::new(
4607 Box::new(Vec::new()),
4608 Box::new(Cursor::new(Vec::new())),
4609 "test-session".into(),
4610 );
4611 let pending = bridge::PendingPromises::new();
4612
4613 let _fn_store;
4614 {
4615 let scope = &mut v8::HandleScope::new(&mut iso);
4616 let local = v8::Local::new(scope, &ctx);
4617 let scope = &mut v8::ContextScope::new(scope, local);
4618 _fn_store = bridge::register_async_bridge_fns(
4619 scope,
4620 &bridge_ctx as *const BridgeCallContext,
4621 &pending as *const bridge::PendingPromises,
4622 &["_asyncFn"],
4623 );
4624 }
4625
4626 eval(
4628 &mut iso,
4629 &ctx,
4630 "var _thenRan = false; _asyncFn().then(function() { _thenRan = true; })",
4631 );
4632
4633 assert!(eval_bool(&mut iso, &ctx, "_thenRan === false"));
4635
4636 {
4638 let scope = &mut v8::HandleScope::new(&mut iso);
4639 let local = v8::Local::new(scope, &ctx);
4640 let scope = &mut v8::ContextScope::new(scope, local);
4641 bridge::resolve_pending_promise(scope, &pending, 1, 0, None, None).unwrap();
4642 }
4643
4644 assert!(eval_bool(&mut iso, &ctx, "_thenRan === true"));
4646 }
4647
4648 {
4650 let mut iso = isolate::create_isolate(None);
4651 let ctx = isolate::create_context(&mut iso);
4652
4653 let (code, error) = {
4654 let scope = &mut v8::HandleScope::new(&mut iso);
4655 let local = v8::Local::new(scope, &ctx);
4656 let scope = &mut v8::ContextScope::new(scope, local);
4657 execute_script(scope, "", "var x = 1 + 2;", &mut None)
4658 };
4659
4660 assert_eq!(code, 0);
4661 assert!(error.is_none());
4662 assert_eq!(eval(&mut iso, &ctx, "x"), "3");
4664 }
4665
4666 {
4668 let mut iso = isolate::create_isolate(None);
4669 let ctx = isolate::create_context(&mut iso);
4670
4671 let bridge = "(function() { globalThis._bridgeReady = true; })()";
4672 let user = "var _sawBridge = _bridgeReady;";
4673 let (code, error) = {
4674 let scope = &mut v8::HandleScope::new(&mut iso);
4675 let local = v8::Local::new(scope, &ctx);
4676 let scope = &mut v8::ContextScope::new(scope, local);
4677 execute_script(scope, bridge, user, &mut None)
4678 };
4679
4680 assert_eq!(code, 0);
4681 assert!(error.is_none());
4682 assert!(eval_bool(&mut iso, &ctx, "_sawBridge === true"));
4683 assert!(eval_bool(&mut iso, &ctx, "_bridgeReady === true"));
4684 }
4685
4686 {
4688 let mut iso = isolate::create_isolate(None);
4689 let ctx = isolate::create_context(&mut iso);
4690
4691 let (code, error) = {
4692 let scope = &mut v8::HandleScope::new(&mut iso);
4693 let local = v8::Local::new(scope, &ctx);
4694 let scope = &mut v8::ContextScope::new(scope, local);
4695 execute_script(
4696 scope,
4697 "",
4698 "(async function () { throw new Error('async failure'); })()",
4699 &mut None,
4700 )
4701 };
4702
4703 assert_eq!(code, 1);
4704 let err = error.unwrap();
4705 assert_eq!(err.error_type, "Error");
4706 assert_eq!(err.message, "async failure");
4707 }
4708
4709 {
4711 let mut iso = isolate::create_isolate(None);
4712 let ctx = isolate::create_context(&mut iso);
4713
4714 let (code, error) = {
4715 let scope = &mut v8::HandleScope::new(&mut iso);
4716 let local = v8::Local::new(scope, &ctx);
4717 let scope = &mut v8::ContextScope::new(scope, local);
4718 execute_script(scope, "", "var x = {;", &mut None)
4719 };
4720
4721 assert_eq!(code, 1);
4722 let err = error.unwrap();
4723 assert_eq!(err.error_type, "SyntaxError");
4724 assert!(!err.message.is_empty());
4725 }
4726
4727 {
4729 let mut iso = isolate::create_isolate(None);
4730 let ctx = isolate::create_context(&mut iso);
4731
4732 let (code, error) = {
4733 let scope = &mut v8::HandleScope::new(&mut iso);
4734 let local = v8::Local::new(scope, &ctx);
4735 let scope = &mut v8::ContextScope::new(scope, local);
4736 execute_script(scope, "", "null.foo", &mut None)
4737 };
4738
4739 assert_eq!(code, 1);
4740 let err = error.unwrap();
4741 assert_eq!(err.error_type, "TypeError");
4742 assert!(!err.message.is_empty());
4743 assert!(!err.stack.is_empty());
4744 }
4745
4746 {
4748 let mut iso = isolate::create_isolate(None);
4749 let ctx = isolate::create_context(&mut iso);
4750
4751 let (code, error) = {
4752 let scope = &mut v8::HandleScope::new(&mut iso);
4753 let local = v8::Local::new(scope, &ctx);
4754 let scope = &mut v8::ContextScope::new(scope, local);
4755 execute_script(scope, "function {", "var x = 1;", &mut None)
4756 };
4757
4758 assert_eq!(code, 1);
4759 let err = error.unwrap();
4760 assert_eq!(err.error_type, "SyntaxError");
4761 assert!(eval_bool(&mut iso, &ctx, "typeof x === 'undefined'"));
4763 }
4764
4765 {
4767 let mut iso = isolate::create_isolate(None);
4768 let ctx = isolate::create_context(&mut iso);
4769
4770 let (code, error) = {
4771 let scope = &mut v8::HandleScope::new(&mut iso);
4772 let local = v8::Local::new(scope, &ctx);
4773 let scope = &mut v8::ContextScope::new(scope, local);
4774 execute_script(scope, "", "'hello'", &mut None)
4775 };
4776
4777 assert_eq!(code, 0);
4778 assert!(error.is_none());
4779 }
4780
4781 {
4783 let mut iso = isolate::create_isolate(None);
4784 let ctx = isolate::create_context(&mut iso);
4785
4786 let (code, error) = {
4787 let scope = &mut v8::HandleScope::new(&mut iso);
4788 let local = v8::Local::new(scope, &ctx);
4789 let scope = &mut v8::ContextScope::new(scope, local);
4790 execute_script(
4791 scope,
4792 "",
4793 "var e = new Error('not found'); e.code = 'ERR_MODULE_NOT_FOUND'; throw e;",
4794 &mut None,
4795 )
4796 };
4797
4798 assert_eq!(code, 1);
4799 let err = error.unwrap();
4800 assert_eq!(err.error_type, "Error");
4801 assert_eq!(err.message, "not found");
4802 assert_eq!(err.code, Some("ERR_MODULE_NOT_FOUND".into()));
4803 }
4804
4805 {
4807 let mut iso = isolate::create_isolate(None);
4808 let ctx = isolate::create_context(&mut iso);
4809
4810 let (code, error) = {
4811 let scope = &mut v8::HandleScope::new(&mut iso);
4812 let local = v8::Local::new(scope, &ctx);
4813 let scope = &mut v8::ContextScope::new(scope, local);
4814 execute_script(scope, "", "throw 'raw string error';", &mut None)
4815 };
4816
4817 assert_eq!(code, 1);
4818 let err = error.unwrap();
4819 assert_eq!(err.error_type, "Error");
4820 assert_eq!(err.message, "raw string error");
4821 assert!(err.stack.is_empty());
4822 assert!(err.code.is_none());
4823 }
4824
4825 {
4827 let mut iso = isolate::create_isolate(None);
4828 let ctx = isolate::create_context(&mut iso);
4829
4830 let bridge_ctx = BridgeCallContext::new(
4831 Box::new(Vec::new()),
4832 Box::new(Cursor::new(Vec::new())),
4833 "test-session".into(),
4834 );
4835
4836 let user_code = "export const x = 42;\nexport const msg = 'hello';";
4837 let (code, exports, error) = {
4838 let scope = &mut v8::HandleScope::new(&mut iso);
4839 let local = v8::Local::new(scope, &ctx);
4840 let scope = &mut v8::ContextScope::new(scope, local);
4841 execute_module(scope, &bridge_ctx, "", user_code, None, &mut None)
4842 };
4843
4844 assert_eq!(code, 0);
4845 assert!(error.is_none());
4846 let exports = exports.unwrap();
4847 {
4848 let scope = &mut v8::HandleScope::new(&mut iso);
4849 let local = v8::Local::new(scope, &ctx);
4850 let scope = &mut v8::ContextScope::new(scope, local);
4851 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4852 assert!(val.is_object());
4853 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4854 let k = v8::String::new(scope, "x").unwrap();
4855 assert_eq!(
4856 obj.get(scope, k.into())
4857 .unwrap()
4858 .int32_value(scope)
4859 .unwrap(),
4860 42
4861 );
4862 let k = v8::String::new(scope, "msg").unwrap();
4863 assert_eq!(
4864 obj.get(scope, k.into())
4865 .unwrap()
4866 .to_rust_string_lossy(scope),
4867 "hello"
4868 );
4869 }
4870 }
4871
4872 {
4874 let mut iso = isolate::create_isolate(None);
4875 let ctx = isolate::create_context(&mut iso);
4876
4877 let bridge_ctx = BridgeCallContext::new(
4878 Box::new(Vec::new()),
4879 Box::new(Cursor::new(Vec::new())),
4880 "test-session".into(),
4881 );
4882
4883 let bridge_code = r#"
4884 globalThis.fetch = async function () { return "ok"; };
4885 "#;
4886 let user_code = r#"
4887 const result = await fetch();
4888 export const fetchType = typeof fetch;
4889 export default result;
4890 "#;
4891 let (code, exports, error) = {
4892 let scope = &mut v8::HandleScope::new(&mut iso);
4893 let local = v8::Local::new(scope, &ctx);
4894 let scope = &mut v8::ContextScope::new(scope, local);
4895 execute_module(scope, &bridge_ctx, bridge_code, user_code, None, &mut None)
4896 };
4897
4898 assert_eq!(code, 0, "error: {:?}", error);
4899 assert!(error.is_none());
4900 let exports = exports.unwrap();
4901 {
4902 let scope = &mut v8::HandleScope::new(&mut iso);
4903 let local = v8::Local::new(scope, &ctx);
4904 let scope = &mut v8::ContextScope::new(scope, local);
4905 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4906 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4907
4908 let fetch_type_key = v8::String::new(scope, "fetchType").unwrap();
4909 assert_eq!(
4910 obj.get(scope, fetch_type_key.into())
4911 .unwrap()
4912 .to_rust_string_lossy(scope),
4913 "function"
4914 );
4915
4916 let default_key = v8::String::new(scope, "default").unwrap();
4917 assert_eq!(
4918 obj.get(scope, default_key.into())
4919 .unwrap()
4920 .to_rust_string_lossy(scope),
4921 "ok"
4922 );
4923 }
4924 }
4925
4926 {
4928 let mut iso = isolate::create_isolate(None);
4929 let ctx = isolate::create_context(&mut iso);
4930
4931 let bridge_ctx = BridgeCallContext::new(
4932 Box::new(Vec::new()),
4933 Box::new(Cursor::new(Vec::new())),
4934 "test-session".into(),
4935 );
4936
4937 let (code, exports, error) = {
4938 let scope = &mut v8::HandleScope::new(&mut iso);
4939 let local = v8::Local::new(scope, &ctx);
4940 let scope = &mut v8::ContextScope::new(scope, local);
4941 execute_module(
4942 scope,
4943 &bridge_ctx,
4944 "",
4945 "export default 'world';",
4946 None,
4947 &mut None,
4948 )
4949 };
4950
4951 assert_eq!(code, 0);
4952 assert!(error.is_none());
4953 let exports = exports.unwrap();
4954 {
4955 let scope = &mut v8::HandleScope::new(&mut iso);
4956 let local = v8::Local::new(scope, &ctx);
4957 let scope = &mut v8::ContextScope::new(scope, local);
4958 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
4959 assert!(val.is_object());
4960 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
4961 let k = v8::String::new(scope, "default").unwrap();
4962 assert_eq!(
4963 obj.get(scope, k.into())
4964 .unwrap()
4965 .to_rust_string_lossy(scope),
4966 "world"
4967 );
4968 }
4969 }
4970
4971 {
4973 let mut iso = isolate::create_isolate(None);
4974 let ctx = isolate::create_context(&mut iso);
4975
4976 let bridge_ctx = BridgeCallContext::new(
4977 Box::new(Vec::new()),
4978 Box::new(Cursor::new(Vec::new())),
4979 "test-session".into(),
4980 );
4981
4982 let (code, _exports, error) = {
4983 let scope = &mut v8::HandleScope::new(&mut iso);
4984 let local = v8::Local::new(scope, &ctx);
4985 let scope = &mut v8::ContextScope::new(scope, local);
4986 execute_module(
4987 scope,
4988 &bridge_ctx,
4989 "",
4990 "export const x = {;",
4991 None,
4992 &mut None,
4993 )
4994 };
4995
4996 assert_eq!(code, 1);
4997 let err = error.unwrap();
4998 assert_eq!(err.error_type, "SyntaxError");
4999 }
5000
5001 {
5003 let mut iso = isolate::create_isolate(None);
5004 let ctx = isolate::create_context(&mut iso);
5005
5006 let bridge_ctx = BridgeCallContext::new(
5007 Box::new(Vec::new()),
5008 Box::new(Cursor::new(Vec::new())),
5009 "test-session".into(),
5010 );
5011
5012 let (code, _exports, error) = {
5013 let scope = &mut v8::HandleScope::new(&mut iso);
5014 let local = v8::Local::new(scope, &ctx);
5015 let scope = &mut v8::ContextScope::new(scope, local);
5016 execute_module(
5017 scope,
5018 &bridge_ctx,
5019 "",
5020 "const x = null; x.foo;",
5021 None,
5022 &mut None,
5023 )
5024 };
5025
5026 assert_eq!(code, 1);
5027 let err = error.unwrap();
5028 assert_eq!(err.error_type, "TypeError");
5029 }
5030
5031 {
5033 let mut iso = isolate::create_isolate(None);
5034 let ctx = isolate::create_context(&mut iso);
5035
5036 let bridge_ctx = BridgeCallContext::new(
5037 Box::new(Vec::new()),
5038 Box::new(Cursor::new(Vec::new())),
5039 "test-session".into(),
5040 );
5041
5042 let bridge = "(function() { globalThis._bridgeReady = true; })()";
5043 let user = "export const saw = _bridgeReady;";
5044 let (code, exports, error) = {
5045 let scope = &mut v8::HandleScope::new(&mut iso);
5046 let local = v8::Local::new(scope, &ctx);
5047 let scope = &mut v8::ContextScope::new(scope, local);
5048 execute_module(scope, &bridge_ctx, bridge, user, None, &mut None)
5049 };
5050
5051 assert_eq!(code, 0);
5052 assert!(error.is_none());
5053 let exports = exports.unwrap();
5054 {
5055 let scope = &mut v8::HandleScope::new(&mut iso);
5056 let local = v8::Local::new(scope, &ctx);
5057 let scope = &mut v8::ContextScope::new(scope, local);
5058 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
5059 assert!(val.is_object());
5060 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
5061 let k = v8::String::new(scope, "saw").unwrap();
5062 assert!(obj.get(scope, k.into()).unwrap().is_true());
5063 }
5064 }
5065
5066 {
5068 let mut iso = isolate::create_isolate(None);
5069 let ctx = isolate::create_context(&mut iso);
5070
5071 let mut response_buf = Vec::new();
5074
5075 let batch_result = v8_serialize_eval(
5076 &mut iso,
5077 &ctx,
5078 "[{resolved: '/dep.mjs', source: 'export const dep_val = 99;'}]",
5079 );
5080 crate::ipc_binary::write_frame(
5081 &mut response_buf,
5082 &crate::ipc_binary::BinaryFrame::BridgeResponse {
5083 session_id: String::new(),
5084 call_id: 1,
5085 status: 0,
5086 payload: batch_result,
5087 },
5088 )
5089 .unwrap();
5090 crate::ipc_binary::write_frame(
5091 &mut response_buf,
5092 &crate::ipc_binary::BinaryFrame::BridgeResponse {
5093 session_id: String::new(),
5094 call_id: 2,
5095 status: 0,
5096 payload: v8_serialize_str(&mut iso, &ctx, "module"),
5097 },
5098 )
5099 .unwrap();
5100 crate::ipc_binary::write_frame(
5101 &mut response_buf,
5102 &crate::ipc_binary::BinaryFrame::BridgeResponse {
5103 session_id: String::new(),
5104 call_id: 3,
5105 status: 0,
5106 payload: v8_serialize_str(&mut iso, &ctx, "module"),
5107 },
5108 )
5109 .unwrap();
5110 crate::ipc_binary::write_frame(
5111 &mut response_buf,
5112 &crate::ipc_binary::BinaryFrame::BridgeResponse {
5113 session_id: String::new(),
5114 call_id: 2,
5115 status: 0,
5116 payload: v8_serialize_str(&mut iso, &ctx, "module"),
5117 },
5118 )
5119 .unwrap();
5120
5121 let bridge_ctx = BridgeCallContext::new(
5122 Box::new(Vec::new()),
5123 Box::new(Cursor::new(response_buf)),
5124 "test-session".into(),
5125 );
5126
5127 let user_code =
5128 "import { dep_val } from './dep.mjs';\nexport const result = dep_val + 1;";
5129 let (code, exports, error) = {
5130 let scope = &mut v8::HandleScope::new(&mut iso);
5131 let local = v8::Local::new(scope, &ctx);
5132 let scope = &mut v8::ContextScope::new(scope, local);
5133 execute_module(
5134 scope,
5135 &bridge_ctx,
5136 "",
5137 user_code,
5138 Some("/app/main.mjs"),
5139 &mut None,
5140 )
5141 };
5142
5143 assert_eq!(code, 0, "error: {:?}", error);
5144 assert!(error.is_none());
5145 let exports = exports.unwrap();
5146 {
5147 let scope = &mut v8::HandleScope::new(&mut iso);
5148 let local = v8::Local::new(scope, &ctx);
5149 let scope = &mut v8::ContextScope::new(scope, local);
5150 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
5151 assert!(val.is_object());
5152 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
5153 let k = v8::String::new(scope, "result").unwrap();
5154 assert_eq!(
5155 obj.get(scope, k.into())
5156 .unwrap()
5157 .int32_value(scope)
5158 .unwrap(),
5159 100
5160 );
5161 }
5162 }
5163
5164 {
5166 let mut iso = isolate::create_isolate(None);
5167 let ctx = isolate::create_context(&mut iso);
5168
5169 let bridge_ctx = BridgeCallContext::new(
5170 Box::new(Vec::new()),
5171 Box::new(Cursor::new(Vec::new())),
5172 "test-session".into(),
5173 );
5174 let pending = bridge::PendingPromises::new();
5175
5176 let _fn_store;
5178 {
5179 let scope = &mut v8::HandleScope::new(&mut iso);
5180 let local = v8::Local::new(scope, &ctx);
5181 let scope = &mut v8::ContextScope::new(scope, local);
5182 _fn_store = bridge::register_async_bridge_fns(
5183 scope,
5184 &bridge_ctx as *const BridgeCallContext,
5185 &pending as *const bridge::PendingPromises,
5186 &["_asyncFn"],
5187 );
5188 }
5189
5190 eval(
5192 &mut iso,
5193 &ctx,
5194 "var _eventLoopResult = 'pending'; _asyncFn('test').then(function(v) { _eventLoopResult = v; })",
5195 );
5196 assert_eq!(pending.len(), 1);
5197 assert_eq!(eval(&mut iso, &ctx, "_eventLoopResult"), "pending");
5198
5199 let (tx, rx) = crossbeam_channel::unbounded();
5201 let result_v8 = v8_serialize_str(&mut iso, &ctx, "event-loop-resolved");
5202 tx.send(crate::session::SessionCommand::Message(
5203 crate::runtime_protocol::SessionMessage::BridgeResponse(
5204 crate::runtime_protocol::BridgeResponse {
5205 call_id: 1,
5206 status: 0,
5207 payload: result_v8,
5208 },
5209 ),
5210 ))
5211 .unwrap();
5212
5213 let completed = {
5215 let scope = &mut v8::HandleScope::new(&mut iso);
5216 let local = v8::Local::new(scope, &ctx);
5217 let scope = &mut v8::ContextScope::new(scope, local);
5218 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5219 };
5220
5221 assert!(
5222 matches!(completed, crate::session::EventLoopStatus::Completed),
5223 "event loop should complete normally"
5224 );
5225 assert_eq!(pending.len(), 0);
5226 assert_eq!(
5227 eval(&mut iso, &ctx, "_eventLoopResult"),
5228 "event-loop-resolved"
5229 );
5230 }
5231
5232 {
5234 let mut iso = isolate::create_isolate(None);
5235 let ctx = isolate::create_context(&mut iso);
5236
5237 let bridge_ctx = BridgeCallContext::new(
5238 Box::new(Vec::new()),
5239 Box::new(Cursor::new(Vec::new())),
5240 "test-session".into(),
5241 );
5242 let pending = bridge::PendingPromises::new();
5243
5244 let _fn_store;
5245 {
5246 let scope = &mut v8::HandleScope::new(&mut iso);
5247 let local = v8::Local::new(scope, &ctx);
5248 let scope = &mut v8::ContextScope::new(scope, local);
5249 _fn_store = bridge::register_async_bridge_fns(
5250 scope,
5251 &bridge_ctx as *const BridgeCallContext,
5252 &pending as *const bridge::PendingPromises,
5253 &["_fetch", "_dns"],
5254 );
5255 }
5256
5257 eval(
5259 &mut iso,
5260 &ctx,
5261 "var _r1 = 'pending'; var _r2 = 'pending'; \
5262 _fetch('url').then(function(v) { _r1 = v; }); \
5263 _dns('host').then(function(v) { _r2 = v; })",
5264 );
5265 assert_eq!(pending.len(), 2);
5266
5267 let (tx, rx) = crossbeam_channel::unbounded();
5269 let r2 = v8_serialize_str(&mut iso, &ctx, "dns-result");
5271 tx.send(crate::session::SessionCommand::Message(
5272 crate::runtime_protocol::SessionMessage::BridgeResponse(
5273 crate::runtime_protocol::BridgeResponse {
5274 call_id: 2,
5275 status: 0,
5276 payload: r2,
5277 },
5278 ),
5279 ))
5280 .unwrap();
5281 let r1 = v8_serialize_str(&mut iso, &ctx, "fetch-result");
5282 tx.send(crate::session::SessionCommand::Message(
5283 crate::runtime_protocol::SessionMessage::BridgeResponse(
5284 crate::runtime_protocol::BridgeResponse {
5285 call_id: 1,
5286 status: 0,
5287 payload: r1,
5288 },
5289 ),
5290 ))
5291 .unwrap();
5292
5293 let completed = {
5294 let scope = &mut v8::HandleScope::new(&mut iso);
5295 let local = v8::Local::new(scope, &ctx);
5296 let scope = &mut v8::ContextScope::new(scope, local);
5297 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5298 };
5299
5300 assert!(matches!(
5301 completed,
5302 crate::session::EventLoopStatus::Completed
5303 ));
5304 assert_eq!(pending.len(), 0);
5305 assert_eq!(eval(&mut iso, &ctx, "_r1"), "fetch-result");
5306 assert_eq!(eval(&mut iso, &ctx, "_r2"), "dns-result");
5307 }
5308
5309 {
5311 let mut iso = isolate::create_isolate(None);
5312 let ctx = isolate::create_context(&mut iso);
5313
5314 let bridge_ctx = BridgeCallContext::new(
5315 Box::new(Vec::new()),
5316 Box::new(Cursor::new(Vec::new())),
5317 "test-session".into(),
5318 );
5319 let pending = bridge::PendingPromises::new();
5320
5321 let _fn_store;
5322 {
5323 let scope = &mut v8::HandleScope::new(&mut iso);
5324 let local = v8::Local::new(scope, &ctx);
5325 let scope = &mut v8::ContextScope::new(scope, local);
5326 _fn_store = bridge::register_async_bridge_fns(
5327 scope,
5328 &bridge_ctx as *const BridgeCallContext,
5329 &pending as *const bridge::PendingPromises,
5330 &["_asyncFn"],
5331 );
5332 }
5333
5334 eval(&mut iso, &ctx, "_asyncFn('test')");
5335 assert_eq!(pending.len(), 1);
5336
5337 let (tx, rx) = crossbeam_channel::unbounded();
5339 tx.send(crate::session::SessionCommand::Message(
5340 crate::runtime_protocol::SessionMessage::TerminateExecution,
5341 ))
5342 .unwrap();
5343
5344 let completed = {
5345 let scope = &mut v8::HandleScope::new(&mut iso);
5346 let local = v8::Local::new(scope, &ctx);
5347 let scope = &mut v8::ContextScope::new(scope, local);
5348 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5349 };
5350
5351 assert!(
5352 matches!(completed, crate::session::EventLoopStatus::Terminated),
5353 "event loop should return terminated status on termination"
5354 );
5355 assert_eq!(pending.len(), 1);
5357
5358 iso.cancel_terminate_execution();
5360 }
5361
5362 {
5364 let mut iso = isolate::create_isolate(None);
5365 let ctx = isolate::create_context(&mut iso);
5366
5367 let bridge_ctx = BridgeCallContext::new(
5368 Box::new(Vec::new()),
5369 Box::new(Cursor::new(Vec::new())),
5370 "test-session".into(),
5371 );
5372 let pending = bridge::PendingPromises::new();
5373
5374 let _fn_store;
5375 {
5376 let scope = &mut v8::HandleScope::new(&mut iso);
5377 let local = v8::Local::new(scope, &ctx);
5378 let scope = &mut v8::ContextScope::new(scope, local);
5379 _fn_store = bridge::register_async_bridge_fns(
5380 scope,
5381 &bridge_ctx as *const BridgeCallContext,
5382 &pending as *const bridge::PendingPromises,
5383 &["_asyncFn"],
5384 );
5385 }
5386
5387 eval(&mut iso, &ctx, "_asyncFn('test')");
5388 assert_eq!(pending.len(), 1);
5389
5390 let (tx, rx) = crossbeam_channel::unbounded();
5392 tx.send(crate::session::SessionCommand::Shutdown).unwrap();
5393
5394 let completed = {
5395 let scope = &mut v8::HandleScope::new(&mut iso);
5396 let local = v8::Local::new(scope, &ctx);
5397 let scope = &mut v8::ContextScope::new(scope, local);
5398 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5399 };
5400
5401 assert!(
5402 matches!(completed, crate::session::EventLoopStatus::Terminated),
5403 "event loop should return terminated status on shutdown"
5404 );
5405 }
5406
5407 {
5409 let mut iso = isolate::create_isolate(None);
5410 let ctx = isolate::create_context(&mut iso);
5411 let pending = bridge::PendingPromises::new();
5412
5413 let (_tx, rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
5414
5415 let completed = {
5417 let scope = &mut v8::HandleScope::new(&mut iso);
5418 let local = v8::Local::new(scope, &ctx);
5419 let scope = &mut v8::ContextScope::new(scope, local);
5420 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5421 };
5422
5423 assert!(matches!(
5424 completed,
5425 crate::session::EventLoopStatus::Completed
5426 ));
5427 }
5428
5429 {
5431 let mut iso = isolate::create_isolate(None);
5432 let ctx = isolate::create_context(&mut iso);
5433
5434 let bridge_ctx = BridgeCallContext::new(
5435 Box::new(Vec::new()),
5436 Box::new(Cursor::new(Vec::new())),
5437 "test-session".into(),
5438 );
5439 let pending = bridge::PendingPromises::new();
5440
5441 let _fn_store;
5442 {
5443 let scope = &mut v8::HandleScope::new(&mut iso);
5444 let local = v8::Local::new(scope, &ctx);
5445 let scope = &mut v8::ContextScope::new(scope, local);
5446 _fn_store = bridge::register_async_bridge_fns(
5447 scope,
5448 &bridge_ctx as *const BridgeCallContext,
5449 &pending as *const bridge::PendingPromises,
5450 &["_asyncFn"],
5451 );
5452 }
5453
5454 eval(
5456 &mut iso,
5457 &ctx,
5458 "var _streamEvents = []; \
5459 globalThis._childProcessDispatch = function(eventType, payload) { \
5460 _streamEvents.push({ type: eventType, data: payload }); \
5461 }; \
5462 _asyncFn('keep-alive')",
5463 );
5464 assert_eq!(pending.len(), 1);
5465
5466 let (tx, rx) = crossbeam_channel::unbounded();
5468
5469 let payload_bytes = v8_serialize_str(&mut iso, &ctx, "hello from child");
5471
5472 tx.send(crate::session::SessionCommand::Message(
5473 crate::runtime_protocol::SessionMessage::StreamEvent(
5474 crate::runtime_protocol::StreamEvent {
5475 event_type: "child_stdout".into(),
5476 payload: payload_bytes,
5477 },
5478 ),
5479 ))
5480 .unwrap();
5481
5482 let r = v8_serialize_null(&mut iso, &ctx);
5484 tx.send(crate::session::SessionCommand::Message(
5485 crate::runtime_protocol::SessionMessage::BridgeResponse(
5486 crate::runtime_protocol::BridgeResponse {
5487 call_id: 1,
5488 status: 0,
5489 payload: r,
5490 },
5491 ),
5492 ))
5493 .unwrap();
5494
5495 let completed = {
5496 let scope = &mut v8::HandleScope::new(&mut iso);
5497 let local = v8::Local::new(scope, &ctx);
5498 let scope = &mut v8::ContextScope::new(scope, local);
5499 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5500 };
5501
5502 assert!(matches!(
5503 completed,
5504 crate::session::EventLoopStatus::Completed
5505 ));
5506 assert_eq!(pending.len(), 0);
5507
5508 assert_eq!(eval(&mut iso, &ctx, "_streamEvents.length"), "1");
5510 assert_eq!(
5511 eval(&mut iso, &ctx, "_streamEvents[0].type"),
5512 "child_stdout"
5513 );
5514 assert_eq!(
5515 eval(&mut iso, &ctx, "_streamEvents[0].data"),
5516 "hello from child"
5517 );
5518 }
5519
5520 {
5522 let mut iso = isolate::create_isolate(None);
5523 let ctx = isolate::create_context(&mut iso);
5524
5525 let bridge_ctx = BridgeCallContext::new(
5526 Box::new(Vec::new()),
5527 Box::new(Cursor::new(Vec::new())),
5528 "test-session".into(),
5529 );
5530 let pending = bridge::PendingPromises::new();
5531
5532 let _fn_store;
5533 {
5534 let scope = &mut v8::HandleScope::new(&mut iso);
5535 let local = v8::Local::new(scope, &ctx);
5536 let scope = &mut v8::ContextScope::new(scope, local);
5537 _fn_store = bridge::register_async_bridge_fns(
5538 scope,
5539 &bridge_ctx as *const BridgeCallContext,
5540 &pending as *const bridge::PendingPromises,
5541 &["_asyncFn"],
5542 );
5543 }
5544
5545 eval(
5547 &mut iso,
5548 &ctx,
5549 "var _microtaskRan = false; \
5550 _asyncFn('test').then(function() { _microtaskRan = true; })",
5551 );
5552 assert!(eval_bool(&mut iso, &ctx, "_microtaskRan === false"));
5553
5554 let (tx, rx) = crossbeam_channel::unbounded();
5555 let r = v8_serialize_null(&mut iso, &ctx);
5556 tx.send(crate::session::SessionCommand::Message(
5557 crate::runtime_protocol::SessionMessage::BridgeResponse(
5558 crate::runtime_protocol::BridgeResponse {
5559 call_id: 1,
5560 status: 0,
5561 payload: r,
5562 },
5563 ),
5564 ))
5565 .unwrap();
5566
5567 {
5568 let scope = &mut v8::HandleScope::new(&mut iso);
5569 let local = v8::Local::new(scope, &ctx);
5570 let scope = &mut v8::ContextScope::new(scope, local);
5571 crate::session::run_event_loop(scope, &rx, &pending, None, None);
5572 }
5573
5574 assert!(eval_bool(&mut iso, &ctx, "_microtaskRan === true"));
5576 }
5577
5578 {
5580 let mut iso = isolate::create_isolate(None);
5581 let ctx = isolate::create_context(&mut iso);
5582
5583 let bridge_ctx = BridgeCallContext::new(
5584 Box::new(Vec::new()),
5585 Box::new(Cursor::new(Vec::new())),
5586 "test-session".into(),
5587 );
5588 let pending = bridge::PendingPromises::new();
5589
5590 let _fn_store;
5591 {
5592 let scope = &mut v8::HandleScope::new(&mut iso);
5593 let local = v8::Local::new(scope, &ctx);
5594 let scope = &mut v8::ContextScope::new(scope, local);
5595 _fn_store = bridge::register_async_bridge_fns(
5596 scope,
5597 &bridge_ctx as *const BridgeCallContext,
5598 &pending as *const bridge::PendingPromises,
5599 &["_asyncFn"],
5600 );
5601 }
5602
5603 eval(
5605 &mut iso,
5606 &ctx,
5607 "var _childEvents = []; \
5608 globalThis._childProcessDispatch = function(eventType, payload) { \
5609 _childEvents.push({ type: eventType, data: payload }); \
5610 }; \
5611 _asyncFn('keep-alive')",
5612 );
5613 assert_eq!(pending.len(), 1);
5614
5615 let (tx, rx) = crossbeam_channel::unbounded();
5616
5617 let stderr_payload = v8_serialize_str(&mut iso, &ctx, "error output");
5619 tx.send(crate::session::SessionCommand::Message(
5620 crate::runtime_protocol::SessionMessage::StreamEvent(
5621 crate::runtime_protocol::StreamEvent {
5622 event_type: "child_stderr".into(),
5623 payload: stderr_payload,
5624 },
5625 ),
5626 ))
5627 .unwrap();
5628
5629 let exit_payload = v8_serialize_int(&mut iso, &ctx, 1);
5631 tx.send(crate::session::SessionCommand::Message(
5632 crate::runtime_protocol::SessionMessage::StreamEvent(
5633 crate::runtime_protocol::StreamEvent {
5634 event_type: "child_exit".into(),
5635 payload: exit_payload,
5636 },
5637 ),
5638 ))
5639 .unwrap();
5640
5641 let r = v8_serialize_null(&mut iso, &ctx);
5643 tx.send(crate::session::SessionCommand::Message(
5644 crate::runtime_protocol::SessionMessage::BridgeResponse(
5645 crate::runtime_protocol::BridgeResponse {
5646 call_id: 1,
5647 status: 0,
5648 payload: r,
5649 },
5650 ),
5651 ))
5652 .unwrap();
5653
5654 let completed = {
5655 let scope = &mut v8::HandleScope::new(&mut iso);
5656 let local = v8::Local::new(scope, &ctx);
5657 let scope = &mut v8::ContextScope::new(scope, local);
5658 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5659 };
5660
5661 assert!(matches!(
5662 completed,
5663 crate::session::EventLoopStatus::Completed
5664 ));
5665 assert_eq!(eval(&mut iso, &ctx, "_childEvents.length"), "2");
5666 assert_eq!(eval(&mut iso, &ctx, "_childEvents[0].type"), "child_stderr");
5667 assert_eq!(eval(&mut iso, &ctx, "_childEvents[0].data"), "error output");
5668 assert_eq!(eval(&mut iso, &ctx, "_childEvents[1].type"), "child_exit");
5669 assert_eq!(eval(&mut iso, &ctx, "_childEvents[1].data"), "1");
5670 }
5671
5672 {
5674 let mut iso = isolate::create_isolate(None);
5675 let ctx = isolate::create_context(&mut iso);
5676
5677 let bridge_ctx = BridgeCallContext::new(
5678 Box::new(Vec::new()),
5679 Box::new(Cursor::new(Vec::new())),
5680 "test-session".into(),
5681 );
5682 let pending = bridge::PendingPromises::new();
5683
5684 let _fn_store;
5685 {
5686 let scope = &mut v8::HandleScope::new(&mut iso);
5687 let local = v8::Local::new(scope, &ctx);
5688 let scope = &mut v8::ContextScope::new(scope, local);
5689 _fn_store = bridge::register_async_bridge_fns(
5690 scope,
5691 &bridge_ctx as *const BridgeCallContext,
5692 &pending as *const bridge::PendingPromises,
5693 &["_asyncFn"],
5694 );
5695 }
5696
5697 eval(
5699 &mut iso,
5700 &ctx,
5701 "var _httpEvents = []; \
5702 globalThis._httpServerDispatch = function(eventType, payload) { \
5703 _httpEvents.push({ type: eventType, data: payload }); \
5704 }; \
5705 _asyncFn('keep-alive')",
5706 );
5707 assert_eq!(pending.len(), 1);
5708
5709 let (tx, rx) = crossbeam_channel::unbounded();
5710
5711 let http_payload =
5713 v8_serialize_eval(&mut iso, &ctx, "({method: 'GET', url: '/api/test'})");
5714 tx.send(crate::session::SessionCommand::Message(
5715 crate::runtime_protocol::SessionMessage::StreamEvent(
5716 crate::runtime_protocol::StreamEvent {
5717 event_type: "http_request".into(),
5718 payload: http_payload,
5719 },
5720 ),
5721 ))
5722 .unwrap();
5723
5724 let r = v8_serialize_null(&mut iso, &ctx);
5726 tx.send(crate::session::SessionCommand::Message(
5727 crate::runtime_protocol::SessionMessage::BridgeResponse(
5728 crate::runtime_protocol::BridgeResponse {
5729 call_id: 1,
5730 status: 0,
5731 payload: r,
5732 },
5733 ),
5734 ))
5735 .unwrap();
5736
5737 let completed = {
5738 let scope = &mut v8::HandleScope::new(&mut iso);
5739 let local = v8::Local::new(scope, &ctx);
5740 let scope = &mut v8::ContextScope::new(scope, local);
5741 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5742 };
5743
5744 assert!(matches!(
5745 completed,
5746 crate::session::EventLoopStatus::Completed
5747 ));
5748 assert_eq!(eval(&mut iso, &ctx, "_httpEvents.length"), "1");
5749 assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].type"), "http_request");
5750 assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].data.method"), "GET");
5751 assert_eq!(eval(&mut iso, &ctx, "_httpEvents[0].data.url"), "/api/test");
5752 }
5753
5754 {
5756 let mut iso = isolate::create_isolate(None);
5757 let ctx = isolate::create_context(&mut iso);
5758
5759 let bridge_ctx = BridgeCallContext::new(
5760 Box::new(Vec::new()),
5761 Box::new(Cursor::new(Vec::new())),
5762 "test-session".into(),
5763 );
5764 let pending = bridge::PendingPromises::new();
5765
5766 let _fn_store;
5767 {
5768 let scope = &mut v8::HandleScope::new(&mut iso);
5769 let local = v8::Local::new(scope, &ctx);
5770 let scope = &mut v8::ContextScope::new(scope, local);
5771 _fn_store = bridge::register_async_bridge_fns(
5772 scope,
5773 &bridge_ctx as *const BridgeCallContext,
5774 &pending as *const bridge::PendingPromises,
5775 &["_asyncFn"],
5776 );
5777 }
5778
5779 eval(
5780 &mut iso,
5781 &ctx,
5782 "var _anyDispatched = false; \
5783 globalThis._childProcessDispatch = function() { _anyDispatched = true; }; \
5784 globalThis._httpServerDispatch = function() { _anyDispatched = true; }; \
5785 _asyncFn('keep-alive')",
5786 );
5787 assert_eq!(pending.len(), 1);
5788
5789 let (tx, rx) = crossbeam_channel::unbounded();
5790
5791 let payload = v8_serialize_null(&mut iso, &ctx);
5793 tx.send(crate::session::SessionCommand::Message(
5794 crate::runtime_protocol::SessionMessage::StreamEvent(
5795 crate::runtime_protocol::StreamEvent {
5796 event_type: "unknown_event".into(),
5797 payload,
5798 },
5799 ),
5800 ))
5801 .unwrap();
5802
5803 let r = v8_serialize_null(&mut iso, &ctx);
5805 tx.send(crate::session::SessionCommand::Message(
5806 crate::runtime_protocol::SessionMessage::BridgeResponse(
5807 crate::runtime_protocol::BridgeResponse {
5808 call_id: 1,
5809 status: 0,
5810 payload: r,
5811 },
5812 ),
5813 ))
5814 .unwrap();
5815
5816 let completed = {
5817 let scope = &mut v8::HandleScope::new(&mut iso);
5818 let local = v8::Local::new(scope, &ctx);
5819 let scope = &mut v8::ContextScope::new(scope, local);
5820 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5821 };
5822
5823 assert!(matches!(
5824 completed,
5825 crate::session::EventLoopStatus::Completed
5826 ));
5827 assert!(eval_bool(&mut iso, &ctx, "_anyDispatched === false"));
5829 }
5830
5831 {
5833 let mut iso = isolate::create_isolate(None);
5834 let ctx = isolate::create_context(&mut iso);
5835
5836 let bridge_ctx = BridgeCallContext::new(
5837 Box::new(Vec::new()),
5838 Box::new(Cursor::new(Vec::new())),
5839 "test-session".into(),
5840 );
5841 let pending = bridge::PendingPromises::new();
5842
5843 let _fn_store;
5844 {
5845 let scope = &mut v8::HandleScope::new(&mut iso);
5846 let local = v8::Local::new(scope, &ctx);
5847 let scope = &mut v8::ContextScope::new(scope, local);
5848 _fn_store = bridge::register_async_bridge_fns(
5849 scope,
5850 &bridge_ctx as *const BridgeCallContext,
5851 &pending as *const bridge::PendingPromises,
5852 &["_asyncFn"],
5853 );
5854 }
5855
5856 eval(&mut iso, &ctx, "_asyncFn('keep-alive')");
5858 assert_eq!(pending.len(), 1);
5859
5860 let (tx, rx) = crossbeam_channel::unbounded();
5861
5862 let payload = v8_serialize_str(&mut iso, &ctx, "data");
5864 tx.send(crate::session::SessionCommand::Message(
5865 crate::runtime_protocol::SessionMessage::StreamEvent(
5866 crate::runtime_protocol::StreamEvent {
5867 event_type: "child_stdout".into(),
5868 payload,
5869 },
5870 ),
5871 ))
5872 .unwrap();
5873
5874 let r = v8_serialize_null(&mut iso, &ctx);
5876 tx.send(crate::session::SessionCommand::Message(
5877 crate::runtime_protocol::SessionMessage::BridgeResponse(
5878 crate::runtime_protocol::BridgeResponse {
5879 call_id: 1,
5880 status: 0,
5881 payload: r,
5882 },
5883 ),
5884 ))
5885 .unwrap();
5886
5887 let completed = {
5889 let scope = &mut v8::HandleScope::new(&mut iso);
5890 let local = v8::Local::new(scope, &ctx);
5891 let scope = &mut v8::ContextScope::new(scope, local);
5892 crate::session::run_event_loop(scope, &rx, &pending, None, None)
5893 };
5894
5895 assert!(matches!(
5896 completed,
5897 crate::session::EventLoopStatus::Completed
5898 ));
5899 }
5900
5901 {
5903 let mut iso = isolate::create_isolate(None);
5904 let ctx = isolate::create_context(&mut iso);
5905
5906 let bridge_ctx = BridgeCallContext::new(
5907 Box::new(Vec::new()),
5908 Box::new(Cursor::new(Vec::new())),
5909 "test-session".into(),
5910 );
5911 let pending = bridge::PendingPromises::new();
5912
5913 let _fn_store;
5914 {
5915 let scope = &mut v8::HandleScope::new(&mut iso);
5916 let local = v8::Local::new(scope, &ctx);
5917 let scope = &mut v8::ContextScope::new(scope, local);
5918 _fn_store = bridge::register_async_bridge_fns(
5919 scope,
5920 &bridge_ctx as *const BridgeCallContext,
5921 &pending as *const bridge::PendingPromises,
5922 &["_asyncFn"],
5923 );
5924 }
5925
5926 eval(
5928 &mut iso,
5929 &ctx,
5930 "var _microtaskRanFromStream = false; \
5931 globalThis._childProcessDispatch = function(eventType, payload) { \
5932 Promise.resolve().then(function() { _microtaskRanFromStream = true; }); \
5933 }; \
5934 _asyncFn('keep-alive')",
5935 );
5936 assert_eq!(pending.len(), 1);
5937
5938 let (tx, rx) = crossbeam_channel::unbounded();
5939
5940 let payload = v8_serialize_str(&mut iso, &ctx, "data");
5941 tx.send(crate::session::SessionCommand::Message(
5942 crate::runtime_protocol::SessionMessage::StreamEvent(
5943 crate::runtime_protocol::StreamEvent {
5944 event_type: "child_stdout".into(),
5945 payload,
5946 },
5947 ),
5948 ))
5949 .unwrap();
5950
5951 let r = v8_serialize_null(&mut iso, &ctx);
5953 tx.send(crate::session::SessionCommand::Message(
5954 crate::runtime_protocol::SessionMessage::BridgeResponse(
5955 crate::runtime_protocol::BridgeResponse {
5956 call_id: 1,
5957 status: 0,
5958 payload: r,
5959 },
5960 ),
5961 ))
5962 .unwrap();
5963
5964 {
5965 let scope = &mut v8::HandleScope::new(&mut iso);
5966 let local = v8::Local::new(scope, &ctx);
5967 let scope = &mut v8::ContextScope::new(scope, local);
5968 crate::session::run_event_loop(scope, &rx, &pending, None, None);
5969 }
5970
5971 assert!(eval_bool(
5973 &mut iso,
5974 &ctx,
5975 "_microtaskRanFromStream === true"
5976 ));
5977 }
5978
5979 {
5981 let mut iso = isolate::create_isolate(None);
5982 let ctx = isolate::create_context(&mut iso);
5983
5984 let (abort_tx, _abort_rx) = crossbeam_channel::bounded::<()>(0);
5986
5987 let iso_handle = iso.thread_safe_handle();
5989
5990 let mut guard = crate::timeout::TimeoutGuard::new(50, iso_handle, abort_tx)
5992 .expect("timeout guard should start");
5993
5994 let (code, error) = {
5996 let scope = &mut v8::HandleScope::new(&mut iso);
5997 let local = v8::Local::new(scope, &ctx);
5998 let scope = &mut v8::ContextScope::new(scope, local);
5999 execute_script(scope, "", "while(true) {}", &mut None)
6000 };
6001
6002 assert!(guard.timed_out(), "timeout should have fired");
6003 assert_eq!(code, 1);
6005 assert!(error.is_some());
6006
6007 guard.cancel();
6008 }
6009
6010 {
6012 let mut iso = isolate::create_isolate(None);
6013 let ctx = isolate::create_context(&mut iso);
6014
6015 let (abort_tx, _abort_rx) = crossbeam_channel::bounded::<()>(0);
6016 let iso_handle = iso.thread_safe_handle();
6017
6018 let mut guard = crate::timeout::TimeoutGuard::new(5000, iso_handle, abort_tx)
6020 .expect("timeout guard should start");
6021
6022 let (code, error) = {
6023 let scope = &mut v8::HandleScope::new(&mut iso);
6024 let local = v8::Local::new(scope, &ctx);
6025 let scope = &mut v8::ContextScope::new(scope, local);
6026 execute_script(scope, "", "1 + 1", &mut None)
6027 };
6028
6029 assert!(!guard.timed_out(), "timeout should not have fired");
6030 assert_eq!(code, 0);
6031 assert!(error.is_none());
6032
6033 guard.cancel();
6034 }
6035
6036 {
6038 let mut iso = isolate::create_isolate(None);
6039 let ctx = isolate::create_context(&mut iso);
6040
6041 let (abort_tx, abort_rx) = crossbeam_channel::bounded::<()>(0);
6043 let iso_handle = iso.thread_safe_handle();
6044
6045 let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
6049
6050 let writer_buf = Arc::new(Mutex::new(Vec::new()));
6052
6053 let pending = bridge::PendingPromises::new();
6058
6059 let bridge_ctx = BridgeCallContext::new(
6061 Box::new(SharedWriter(Arc::clone(&writer_buf))),
6062 Box::new(Cursor::new(Vec::new())), "test-session".into(),
6064 );
6065 let _async_store;
6066 {
6067 let scope = &mut v8::HandleScope::new(&mut iso);
6068 let local = v8::Local::new(scope, &ctx);
6069 let scope = &mut v8::ContextScope::new(scope, local);
6070 _async_store = bridge::register_async_bridge_fns(
6071 scope,
6072 &bridge_ctx as *const BridgeCallContext,
6073 &pending as *const bridge::PendingPromises,
6074 &["_slowFn"],
6075 );
6076 }
6077
6078 let (_code, _error) = {
6080 let scope = &mut v8::HandleScope::new(&mut iso);
6081 let local = v8::Local::new(scope, &ctx);
6082 let scope = &mut v8::ContextScope::new(scope, local);
6083 execute_script(scope, "", "_slowFn('never-responds')", &mut None)
6084 };
6085
6086 assert_eq!(pending.len(), 1, "should have 1 pending promise");
6087
6088 let mut guard = crate::timeout::TimeoutGuard::new(50, iso_handle, abort_tx)
6090 .expect("timeout guard should start");
6091
6092 let completed = {
6095 let scope = &mut v8::HandleScope::new(&mut iso);
6096 let local = v8::Local::new(scope, &ctx);
6097 let scope = &mut v8::ContextScope::new(scope, local);
6098 crate::session::run_event_loop(scope, &cmd_rx, &pending, Some(&abort_rx), None)
6099 };
6100
6101 assert!(
6102 matches!(completed, crate::session::EventLoopStatus::Terminated),
6103 "event loop should have been terminated"
6104 );
6105 assert!(guard.timed_out(), "timeout should have fired");
6106
6107 guard.cancel();
6108 drop(cmd_tx); }
6110
6111 {
6113 let err = crate::ipc::ExecutionError {
6116 error_type: "Error".into(),
6117 message: "Script execution timed out".into(),
6118 stack: String::new(),
6119 code: Some("ERR_SCRIPT_EXECUTION_TIMEOUT".into()),
6120 };
6121 assert_eq!(err.error_type, "Error");
6122 assert_eq!(err.message, "Script execution timed out");
6123 assert_eq!(err.code, Some("ERR_SCRIPT_EXECUTION_TIMEOUT".into()));
6124 }
6125
6126 {
6128 let mut iso = isolate::create_isolate(None);
6129 let ctx = isolate::create_context(&mut iso);
6130
6131 let scope = &mut v8::HandleScope::new(&mut iso);
6132 let local = v8::Local::new(scope, &ctx);
6133 let scope = &mut v8::ContextScope::new(scope, local);
6134
6135 let code = r#"
6137 var err = new Error("process.exit(42)");
6138 err._isProcessExit = true;
6139 err.code = 42;
6140 throw err;
6141 "#;
6142
6143 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6144 assert_eq!(
6145 exit_code, 42,
6146 "ProcessExitError should return the error's exit code"
6147 );
6148 let err = error.unwrap();
6149 assert_eq!(err.error_type, "Error");
6150 assert!(err.message.contains("process.exit(42)"));
6151 assert_eq!(err.code, None);
6153 }
6154
6155 {
6157 let mut iso = isolate::create_isolate(None);
6158 let ctx = isolate::create_context(&mut iso);
6159
6160 let scope = &mut v8::HandleScope::new(&mut iso);
6161 let local = v8::Local::new(scope, &ctx);
6162 let scope = &mut v8::ContextScope::new(scope, local);
6163
6164 let code = r#"
6165 var err = new Error("process.exit(0)");
6166 err._isProcessExit = true;
6167 err.code = 0;
6168 throw err;
6169 "#;
6170
6171 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6172 assert_eq!(
6173 exit_code, 0,
6174 "ProcessExitError code 0 should return exit code 0"
6175 );
6176 assert!(error.is_some());
6177 }
6178
6179 {
6181 let mut iso = isolate::create_isolate(None);
6182 let ctx = isolate::create_context(&mut iso);
6183
6184 let scope = &mut v8::HandleScope::new(&mut iso);
6185 let local = v8::Local::new(scope, &ctx);
6186 let scope = &mut v8::ContextScope::new(scope, local);
6187
6188 let code = r#"throw new TypeError("not a process exit")"#;
6190
6191 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6192 assert_eq!(exit_code, 1, "Regular errors should return exit code 1");
6193 let err = error.unwrap();
6194 assert_eq!(err.error_type, "TypeError");
6195 assert_eq!(err.message, "not a process exit");
6196 }
6197
6198 {
6200 let mut iso = isolate::create_isolate(None);
6201 let ctx = isolate::create_context(&mut iso);
6202
6203 let scope = &mut v8::HandleScope::new(&mut iso);
6204 let local = v8::Local::new(scope, &ctx);
6205 let scope = &mut v8::ContextScope::new(scope, local);
6206
6207 let code = r#"
6209 class ProcessExitError extends Error {
6210 constructor(exitCode) {
6211 super("process exited with code " + exitCode);
6212 this._isProcessExit = true;
6213 this.code = exitCode;
6214 }
6215 }
6216 throw new ProcessExitError(7);
6217 "#;
6218
6219 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6220 assert_eq!(exit_code, 7);
6221 let err = error.unwrap();
6222 assert_eq!(err.error_type, "ProcessExitError");
6223 assert!(err.message.contains("process exited with code 7"));
6224 }
6225
6226 {
6228 let mut iso = isolate::create_isolate(None);
6229 let ctx = isolate::create_context(&mut iso);
6230
6231 let scope = &mut v8::HandleScope::new(&mut iso);
6232 let local = v8::Local::new(scope, &ctx);
6233 let scope = &mut v8::ContextScope::new(scope, local);
6234
6235 let code = r#"throw "just a string""#;
6237 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6238 assert_eq!(exit_code, 1);
6239 let err = error.unwrap();
6240 assert_eq!(err.error_type, "Error");
6241 assert_eq!(err.message, "just a string");
6242
6243 let code2 = r#"
6245 var obj = new Error("no sentinel");
6246 obj._isProcessExit = false;
6247 obj.code = 99;
6248 throw obj;
6249 "#;
6250 let (exit_code2, error2) = execute_script(scope, "", code2, &mut None);
6251 assert_eq!(exit_code2, 1, "_isProcessExit:false should not be detected");
6252 assert!(error2.is_some());
6253 }
6254
6255 {
6257 let mut iso = isolate::create_isolate(None);
6258 let ctx = isolate::create_context(&mut iso);
6259
6260 let scope = &mut v8::HandleScope::new(&mut iso);
6261 let local = v8::Local::new(scope, &ctx);
6262 let scope = &mut v8::ContextScope::new(scope, local);
6263
6264 let code = r#"
6265 var err = new Error("Cannot find module './missing'");
6266 err.code = "ERR_MODULE_NOT_FOUND";
6267 throw err;
6268 "#;
6269
6270 let (exit_code, error) = execute_script(scope, "", code, &mut None);
6271 assert_eq!(exit_code, 1);
6272 let err = error.unwrap();
6273 assert_eq!(err.error_type, "Error");
6274 assert_eq!(err.code, Some("ERR_MODULE_NOT_FOUND".into()));
6275 }
6276
6277 {
6279 let mut iso = isolate::create_isolate(None);
6280 let ctx = isolate::create_context(&mut iso);
6281
6282 let scope = &mut v8::HandleScope::new(&mut iso);
6283 let local = v8::Local::new(scope, &ctx);
6284 let scope = &mut v8::ContextScope::new(scope, local);
6285
6286 let (_, err) = execute_script(scope, "", "eval('function(')", &mut None);
6288 let err = err.unwrap();
6289 assert_eq!(err.error_type, "SyntaxError");
6290
6291 let (_, err2) = execute_script(scope, "", "new Array(-1)", &mut None);
6293 let err2 = err2.unwrap();
6294 assert_eq!(err2.error_type, "RangeError");
6295
6296 let (_, err3) = execute_script(scope, "", "undefinedVariable", &mut None);
6298 let err3 = err3.unwrap();
6299 assert_eq!(err3.error_type, "ReferenceError");
6300 }
6301
6302 {
6304 let mut iso = isolate::create_isolate(None);
6305 let ctx = isolate::create_context(&mut iso);
6306
6307 let scope = &mut v8::HandleScope::new(&mut iso);
6308 let local = v8::Local::new(scope, &ctx);
6309 let scope = &mut v8::ContextScope::new(scope, local);
6310 execute_script(
6311 scope,
6312 "",
6313 "globalThis.process = { exitCode: 0 };",
6314 &mut None,
6315 );
6316
6317 let (exit_code, error) = execute_script(scope, "", "process.exitCode = 3;", &mut None);
6318 assert_eq!(exit_code, 3);
6319 assert!(error.is_none());
6320 }
6321
6322 {
6324 let mut iso = isolate::create_isolate(None);
6325 let ctx = isolate::create_context(&mut iso);
6326
6327 let scope = &mut v8::HandleScope::new(&mut iso);
6328 let local = v8::Local::new(scope, &ctx);
6329 let scope = &mut v8::ContextScope::new(scope, local);
6330 execute_script(
6331 scope,
6332 "",
6333 "globalThis.process = { exitCode: 0 };",
6334 &mut None,
6335 );
6336
6337 let (exit_code, error) = execute_script(
6338 scope,
6339 "",
6340 "(async () => { process.exitCode = 4; })()",
6341 &mut None,
6342 );
6343 assert_eq!(exit_code, 4);
6344 assert!(error.is_none());
6345 }
6346
6347 {
6349 let mut iso = isolate::create_isolate(None);
6350 let ctx = isolate::create_context(&mut iso);
6351
6352 let scope = &mut v8::HandleScope::new(&mut iso);
6353 let local = v8::Local::new(scope, &ctx);
6354 let scope = &mut v8::ContextScope::new(scope, local);
6355
6356 let code = r#"
6357 function innerFn() { throw new Error("deep error"); }
6358 function outerFn() { innerFn(); }
6359 outerFn();
6360 "#;
6361
6362 let (_, error) = execute_script(scope, "", code, &mut None);
6363 let err = error.unwrap();
6364 assert_eq!(err.error_type, "Error");
6365 assert_eq!(err.message, "deep error");
6366 assert!(
6367 err.stack.contains("innerFn"),
6368 "stack should contain innerFn"
6369 );
6370 assert!(
6371 err.stack.contains("outerFn"),
6372 "stack should contain outerFn"
6373 );
6374 }
6375
6376 {
6380 use crate::bridge::{
6381 deserialize_v8_wire_value as deserialize_v8_value,
6382 serialize_v8_wire_value as serialize_v8_value,
6383 };
6384
6385 let mut iso = isolate::create_isolate(None);
6386 let ctx = isolate::create_context(&mut iso);
6387 let scope = &mut v8::HandleScope::new(&mut iso);
6388 let local = v8::Local::new(scope, &ctx);
6389 let scope = &mut v8::ContextScope::new(scope, local);
6390
6391 let null_val = v8::null(scope).into();
6393 let bytes = serialize_v8_value(scope, null_val).unwrap();
6394 let out = deserialize_v8_value(scope, &bytes).unwrap();
6395 assert!(out.is_null());
6396
6397 let undef_val = v8::undefined(scope).into();
6399 let bytes = serialize_v8_value(scope, undef_val).unwrap();
6400 let out = deserialize_v8_value(scope, &bytes).unwrap();
6401 assert!(out.is_undefined());
6402
6403 let bool_val = v8::Boolean::new(scope, true).into();
6405 let bytes = serialize_v8_value(scope, bool_val).unwrap();
6406 let out = deserialize_v8_value(scope, &bytes).unwrap();
6407 assert!(out.is_true());
6408
6409 let bool_val = v8::Boolean::new(scope, false).into();
6411 let bytes = serialize_v8_value(scope, bool_val).unwrap();
6412 let out = deserialize_v8_value(scope, &bytes).unwrap();
6413 assert!(out.is_false());
6414
6415 let num_val: v8::Local<v8::Value> = v8::Integer::new(scope, 42).into();
6417 let bytes = serialize_v8_value(scope, num_val).unwrap();
6418 let out = deserialize_v8_value(scope, &bytes).unwrap();
6419 assert_eq!(out.int32_value(scope).unwrap(), 42);
6420
6421 let num_val: v8::Local<v8::Value> = v8::Integer::new(scope, -7).into();
6423 let bytes = serialize_v8_value(scope, num_val).unwrap();
6424 let out = deserialize_v8_value(scope, &bytes).unwrap();
6425 assert_eq!(out.int32_value(scope).unwrap(), -7);
6426
6427 let num_val: v8::Local<v8::Value> = v8::Number::new(scope, 3.125).into();
6429 let bytes = serialize_v8_value(scope, num_val).unwrap();
6430 let out = deserialize_v8_value(scope, &bytes).unwrap();
6431 assert!((out.number_value(scope).unwrap() - 3.125).abs() < 1e-10);
6432 }
6433
6434 {
6436 use crate::bridge::{
6437 deserialize_v8_wire_value as deserialize_v8_value,
6438 serialize_v8_wire_value as serialize_v8_value,
6439 };
6440
6441 let mut iso = isolate::create_isolate(None);
6442 let ctx = isolate::create_context(&mut iso);
6443 let scope = &mut v8::HandleScope::new(&mut iso);
6444 let local = v8::Local::new(scope, &ctx);
6445 let scope = &mut v8::ContextScope::new(scope, local);
6446
6447 let s = v8::String::new(scope, "hello world").unwrap();
6449 let bytes = serialize_v8_value(scope, s.into()).unwrap();
6450 let out = deserialize_v8_value(scope, &bytes).unwrap();
6451 assert!(out.is_string());
6452 assert_eq!(out.to_rust_string_lossy(scope), "hello world");
6453
6454 let s = v8::String::new(scope, "").unwrap();
6456 let bytes = serialize_v8_value(scope, s.into()).unwrap();
6457 let out = deserialize_v8_value(scope, &bytes).unwrap();
6458 assert!(out.is_string());
6459 assert_eq!(out.to_rust_string_lossy(scope), "");
6460
6461 let s = v8::String::new(scope, "hello 🌍 world").unwrap();
6463 let bytes = serialize_v8_value(scope, s.into()).unwrap();
6464 let out = deserialize_v8_value(scope, &bytes).unwrap();
6465 assert_eq!(out.to_rust_string_lossy(scope), "hello 🌍 world");
6466 }
6467
6468 {
6470 use crate::bridge::{
6471 deserialize_v8_wire_value as deserialize_v8_value,
6472 serialize_v8_wire_value as serialize_v8_value,
6473 };
6474
6475 let mut iso = isolate::create_isolate(None);
6476 let ctx = isolate::create_context(&mut iso);
6477 let scope = &mut v8::HandleScope::new(&mut iso);
6478 let local = v8::Local::new(scope, &ctx);
6479 let scope = &mut v8::ContextScope::new(scope, local);
6480
6481 let arr = v8::Array::new(scope, 4);
6483 let v1: v8::Local<v8::Value> = v8::Integer::new(scope, 1).into();
6484 let v2: v8::Local<v8::Value> = v8::String::new(scope, "two").unwrap().into();
6485 let v3: v8::Local<v8::Value> = v8::Boolean::new(scope, true).into();
6486 let v4: v8::Local<v8::Value> = v8::null(scope).into();
6487 arr.set_index(scope, 0, v1);
6488 arr.set_index(scope, 1, v2);
6489 arr.set_index(scope, 2, v3);
6490 arr.set_index(scope, 3, v4);
6491
6492 let bytes = serialize_v8_value(scope, arr.into()).unwrap();
6493 let out = deserialize_v8_value(scope, &bytes).unwrap();
6494 assert!(out.is_array());
6495 let out_arr = v8::Local::<v8::Array>::try_from(out).unwrap();
6496 assert_eq!(out_arr.length(), 4);
6497 assert_eq!(
6498 out_arr
6499 .get_index(scope, 0)
6500 .unwrap()
6501 .int32_value(scope)
6502 .unwrap(),
6503 1
6504 );
6505 assert_eq!(
6506 out_arr
6507 .get_index(scope, 1)
6508 .unwrap()
6509 .to_rust_string_lossy(scope),
6510 "two"
6511 );
6512 assert!(out_arr.get_index(scope, 2).unwrap().is_true());
6513 assert!(out_arr.get_index(scope, 3).unwrap().is_null());
6514
6515 let empty_arr = v8::Array::new(scope, 0);
6517 let bytes = serialize_v8_value(scope, empty_arr.into()).unwrap();
6518 let out = deserialize_v8_value(scope, &bytes).unwrap();
6519 assert!(out.is_array());
6520 assert_eq!(v8::Local::<v8::Array>::try_from(out).unwrap().length(), 0);
6521 }
6522
6523 {
6525 use crate::bridge::{
6526 deserialize_v8_wire_value as deserialize_v8_value,
6527 serialize_v8_wire_value as serialize_v8_value,
6528 };
6529
6530 let mut iso = isolate::create_isolate(None);
6531 let ctx = isolate::create_context(&mut iso);
6532 let scope = &mut v8::HandleScope::new(&mut iso);
6533 let local = v8::Local::new(scope, &ctx);
6534 let scope = &mut v8::ContextScope::new(scope, local);
6535
6536 let obj = v8::Object::new(scope);
6538 let k1 = v8::String::new(scope, "name").unwrap();
6539 let v1: v8::Local<v8::Value> = v8::String::new(scope, "test").unwrap().into();
6540 let k2 = v8::String::new(scope, "count").unwrap();
6541 let v2: v8::Local<v8::Value> = v8::Integer::new(scope, 42).into();
6542 let k3 = v8::String::new(scope, "active").unwrap();
6543 let v3: v8::Local<v8::Value> = v8::Boolean::new(scope, true).into();
6544 obj.set(scope, k1.into(), v1);
6545 obj.set(scope, k2.into(), v2);
6546 obj.set(scope, k3.into(), v3);
6547
6548 let bytes = serialize_v8_value(scope, obj.into()).unwrap();
6549 let out = deserialize_v8_value(scope, &bytes).unwrap();
6550 assert!(out.is_object());
6551 let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6552 let k = v8::String::new(scope, "name").unwrap();
6553 assert_eq!(
6554 out_obj
6555 .get(scope, k.into())
6556 .unwrap()
6557 .to_rust_string_lossy(scope),
6558 "test"
6559 );
6560 let k = v8::String::new(scope, "count").unwrap();
6561 assert_eq!(
6562 out_obj
6563 .get(scope, k.into())
6564 .unwrap()
6565 .int32_value(scope)
6566 .unwrap(),
6567 42
6568 );
6569 let k = v8::String::new(scope, "active").unwrap();
6570 assert!(out_obj.get(scope, k.into()).unwrap().is_true());
6571 }
6572
6573 {
6575 use crate::bridge::{
6576 deserialize_v8_wire_value as deserialize_v8_value,
6577 serialize_v8_wire_value as serialize_v8_value,
6578 };
6579
6580 let mut iso = isolate::create_isolate(None);
6581 let ctx = isolate::create_context(&mut iso);
6582 let scope = &mut v8::HandleScope::new(&mut iso);
6583 let local = v8::Local::new(scope, &ctx);
6584 let scope = &mut v8::ContextScope::new(scope, local);
6585
6586 let data = [0u8, 1, 2, 255, 128, 64];
6587 let ab = v8::ArrayBuffer::new(scope, data.len());
6588 {
6589 let bs = ab.get_backing_store();
6590 unsafe {
6591 std::ptr::copy_nonoverlapping(
6592 data.as_ptr(),
6593 bs.data().unwrap().as_ptr() as *mut u8,
6594 data.len(),
6595 );
6596 }
6597 }
6598 let u8arr = v8::Uint8Array::new(scope, ab, 0, data.len()).unwrap();
6599
6600 let bytes = serialize_v8_value(scope, u8arr.into()).unwrap();
6601 let out = deserialize_v8_value(scope, &bytes).unwrap();
6602 assert!(out.is_uint8_array());
6603 let out_arr = v8::Local::<v8::Uint8Array>::try_from(out).unwrap();
6604 assert_eq!(out_arr.byte_length(), 6);
6605 let mut buf = vec![0u8; 6];
6606 out_arr.copy_contents(&mut buf);
6607 assert_eq!(buf, vec![0, 1, 2, 255, 128, 64]);
6608 }
6609
6610 {
6612 use crate::bridge::{
6613 deserialize_v8_wire_value as deserialize_v8_value,
6614 serialize_v8_wire_value as serialize_v8_value,
6615 };
6616
6617 let mut iso = isolate::create_isolate(None);
6618 let ctx = isolate::create_context(&mut iso);
6619 let scope = &mut v8::HandleScope::new(&mut iso);
6620 let local = v8::Local::new(scope, &ctx);
6621 let scope = &mut v8::ContextScope::new(scope, local);
6622
6623 let code = r#"
6625 ({
6626 items: [1, { nested: "value" }],
6627 flag: false
6628 })
6629 "#;
6630 let source = v8::String::new(scope, code).unwrap();
6631 let script = v8::Script::compile(scope, source, None).unwrap();
6632 let val = script.run(scope).unwrap();
6633
6634 let bytes = serialize_v8_value(scope, val).unwrap();
6635 let out = deserialize_v8_value(scope, &bytes).unwrap();
6636 assert!(out.is_object());
6637 let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6638
6639 let k = v8::String::new(scope, "items").unwrap();
6641 let items = out_obj.get(scope, k.into()).unwrap();
6642 assert!(items.is_array());
6643 let items_arr = v8::Local::<v8::Array>::try_from(items).unwrap();
6644 assert_eq!(items_arr.length(), 2);
6645 assert_eq!(
6646 items_arr
6647 .get_index(scope, 0)
6648 .unwrap()
6649 .int32_value(scope)
6650 .unwrap(),
6651 1
6652 );
6653 let inner = items_arr.get_index(scope, 1).unwrap();
6654 assert!(inner.is_object());
6655 let inner_obj = v8::Local::<v8::Object>::try_from(inner).unwrap();
6656 let k = v8::String::new(scope, "nested").unwrap();
6657 assert_eq!(
6658 inner_obj
6659 .get(scope, k.into())
6660 .unwrap()
6661 .to_rust_string_lossy(scope),
6662 "value"
6663 );
6664
6665 let k = v8::String::new(scope, "flag").unwrap();
6667 assert!(out_obj.get(scope, k.into()).unwrap().is_false());
6668 }
6669
6670 {
6672 use crate::bridge::{
6673 deserialize_v8_wire_value as deserialize_v8_value,
6674 serialize_v8_wire_value as serialize_v8_value,
6675 };
6676
6677 let mut iso = isolate::create_isolate(None);
6678 let ctx = isolate::create_context(&mut iso);
6679 let scope = &mut v8::HandleScope::new(&mut iso);
6680 let local = v8::Local::new(scope, &ctx);
6681 let scope = &mut v8::ContextScope::new(scope, local);
6682
6683 let source = v8::String::new(scope, "new Date(1700000000000)").unwrap();
6685 let script = v8::Script::compile(scope, source, None).unwrap();
6686 let date_val = script.run(scope).unwrap();
6687 let bytes = serialize_v8_value(scope, date_val).unwrap();
6688 let out = deserialize_v8_value(scope, &bytes).unwrap();
6689 assert!(out.is_date());
6690 let date = v8::Local::<v8::Date>::try_from(out).unwrap();
6691 assert_eq!(date.value_of(), 1700000000000.0);
6692
6693 let source = v8::String::new(scope, "/abc/gi").unwrap();
6695 let script = v8::Script::compile(scope, source, None).unwrap();
6696 let re_val = script.run(scope).unwrap();
6697 let bytes = serialize_v8_value(scope, re_val).unwrap();
6698 let out = deserialize_v8_value(scope, &bytes).unwrap();
6699 assert!(out.is_reg_exp());
6700
6701 let source = v8::String::new(scope, "new Map([['a', 1], ['b', 2]])").unwrap();
6703 let script = v8::Script::compile(scope, source, None).unwrap();
6704 let map_val = script.run(scope).unwrap();
6705 let bytes = serialize_v8_value(scope, map_val).unwrap();
6706 let out = deserialize_v8_value(scope, &bytes).unwrap();
6707 assert!(out.is_map());
6708 let map = v8::Local::<v8::Map>::try_from(out).unwrap();
6709 assert_eq!(map.size(), 2);
6710
6711 let source = v8::String::new(scope, "new Set([10, 20, 30])").unwrap();
6713 let script = v8::Script::compile(scope, source, None).unwrap();
6714 let set_val = script.run(scope).unwrap();
6715 let bytes = serialize_v8_value(scope, set_val).unwrap();
6716 let out = deserialize_v8_value(scope, &bytes).unwrap();
6717 assert!(out.is_set());
6718 let set = v8::Local::<v8::Set>::try_from(out).unwrap();
6719 assert_eq!(set.size(), 3);
6720
6721 let source = v8::String::new(scope, "new TypeError('oops')").unwrap();
6723 let script = v8::Script::compile(scope, source, None).unwrap();
6724 let err_val = script.run(scope).unwrap();
6725 let bytes = serialize_v8_value(scope, err_val).unwrap();
6726 let out = deserialize_v8_value(scope, &bytes).unwrap();
6727 assert!(out.is_object());
6729 let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6730 let k = v8::String::new(scope, "message").unwrap();
6731 let msg = out_obj.get(scope, k.into()).unwrap();
6732 assert_eq!(msg.to_rust_string_lossy(scope), "oops");
6733 }
6734
6735 {
6737 use crate::bridge::{
6738 deserialize_v8_wire_value as deserialize_v8_value,
6739 serialize_v8_wire_value as serialize_v8_value,
6740 };
6741
6742 let mut iso = isolate::create_isolate(None);
6743 let ctx = isolate::create_context(&mut iso);
6744 let scope = &mut v8::HandleScope::new(&mut iso);
6745 let local = v8::Local::new(scope, &ctx);
6746 let scope = &mut v8::ContextScope::new(scope, local);
6747
6748 let source = v8::String::new(scope, "var o = { a: 1 }; o.self = o; o").unwrap();
6750 let script = v8::Script::compile(scope, source, None).unwrap();
6751 let circ_val = script.run(scope).unwrap();
6752
6753 let bytes = serialize_v8_value(scope, circ_val).unwrap();
6754 let out = deserialize_v8_value(scope, &bytes).unwrap();
6755 assert!(out.is_object());
6756 let out_obj = v8::Local::<v8::Object>::try_from(out).unwrap();
6757
6758 let k = v8::String::new(scope, "a").unwrap();
6760 assert_eq!(
6761 out_obj
6762 .get(scope, k.into())
6763 .unwrap()
6764 .int32_value(scope)
6765 .unwrap(),
6766 1
6767 );
6768 let k = v8::String::new(scope, "self").unwrap();
6769 let self_ref = out_obj.get(scope, k.into()).unwrap();
6770 assert!(self_ref.is_object());
6771 let self_obj = v8::Local::<v8::Object>::try_from(self_ref).unwrap();
6773 let k = v8::String::new(scope, "a").unwrap();
6774 assert_eq!(
6775 self_obj
6776 .get(scope, k.into())
6777 .unwrap()
6778 .int32_value(scope)
6779 .unwrap(),
6780 1
6781 );
6782 }
6783
6784 {
6788 let mut iso = isolate::create_isolate(None);
6789 let ctx = isolate::create_context(&mut iso);
6790 let mut cache: Option<BridgeCodeCache> = None;
6791
6792 let bridge = "(function() { globalThis._cached = 'yes'; })()";
6793 let (code, error) = {
6794 let scope = &mut v8::HandleScope::new(&mut iso);
6795 let local = v8::Local::new(scope, &ctx);
6796 let scope = &mut v8::ContextScope::new(scope, local);
6797 execute_script(scope, bridge, "var _saw = _cached;", &mut cache)
6798 };
6799
6800 assert_eq!(code, 0);
6801 assert!(error.is_none());
6802 assert_eq!(eval(&mut iso, &ctx, "_saw"), "yes");
6803 assert!(
6805 cache.is_some(),
6806 "cache should be populated after first execution"
6807 );
6808 assert!(!cache.as_ref().unwrap().cached_data.is_empty());
6809 }
6810
6811 {
6813 let mut iso = isolate::create_isolate(None);
6814 let mut cache: Option<BridgeCodeCache> = None;
6815 let bridge = "(function() { globalThis._counter = (globalThis._counter || 0) + 1; })()";
6816
6817 {
6819 let ctx = isolate::create_context(&mut iso);
6820 let (code, _) = {
6821 let scope = &mut v8::HandleScope::new(&mut iso);
6822 let local = v8::Local::new(scope, &ctx);
6823 let scope = &mut v8::ContextScope::new(scope, local);
6824 execute_script(scope, bridge, "", &mut cache)
6825 };
6826 assert_eq!(code, 0);
6827 assert!(cache.is_some());
6828 }
6829
6830 let cached_data_len = cache.as_ref().unwrap().cached_data.len();
6831
6832 {
6834 let ctx = isolate::create_context(&mut iso);
6835 let (code, _) = {
6836 let scope = &mut v8::HandleScope::new(&mut iso);
6837 let local = v8::Local::new(scope, &ctx);
6838 let scope = &mut v8::ContextScope::new(scope, local);
6839 execute_script(scope, bridge, "", &mut cache)
6840 };
6841 assert_eq!(code, 0);
6842 assert!(
6844 cache.is_some(),
6845 "cache should persist after second execution"
6846 );
6847 assert_eq!(cache.as_ref().unwrap().cached_data.len(), cached_data_len);
6849 assert_eq!(eval(&mut iso, &ctx, "String(_counter)"), "1");
6851 }
6852 }
6853
6854 {
6856 let mut iso = isolate::create_isolate(None);
6857 let mut cache: Option<BridgeCodeCache> = None;
6858
6859 {
6861 let ctx = isolate::create_context(&mut iso);
6862 let (code, _) = {
6863 let scope = &mut v8::HandleScope::new(&mut iso);
6864 let local = v8::Local::new(scope, &ctx);
6865 let scope = &mut v8::ContextScope::new(scope, local);
6866 execute_script(
6867 scope,
6868 "(function() { globalThis.x = 'A'; })()",
6869 "",
6870 &mut cache,
6871 )
6872 };
6873 assert_eq!(code, 0);
6874 assert!(cache.is_some());
6875 }
6876
6877 let hash_a = cache.as_ref().unwrap().source_hash;
6878
6879 {
6881 let ctx = isolate::create_context(&mut iso);
6882 let (code, _) = {
6883 let scope = &mut v8::HandleScope::new(&mut iso);
6884 let local = v8::Local::new(scope, &ctx);
6885 let scope = &mut v8::ContextScope::new(scope, local);
6886 execute_script(
6887 scope,
6888 "(function() { globalThis.x = 'B'; })()",
6889 "",
6890 &mut cache,
6891 )
6892 };
6893 assert_eq!(code, 0);
6894 assert!(cache.is_some());
6895 assert_ne!(cache.as_ref().unwrap().source_hash, hash_a);
6897 assert_eq!(eval(&mut iso, &ctx, "x"), "B");
6899 }
6900 }
6901
6902 {
6904 let mut iso = isolate::create_isolate(None);
6905 let mut cache: Option<BridgeCodeCache> = None;
6906
6907 let output = Arc::new(Mutex::new(Vec::new()));
6908 let writer = SharedWriter(Arc::clone(&output));
6909 let reader = Cursor::new(Vec::new());
6910 let bridge_ctx =
6911 BridgeCallContext::new(Box::new(writer), Box::new(reader), "test-session".into());
6912
6913 let bridge = "(function() { globalThis._moduleBridge = true; })()";
6914
6915 {
6917 let ctx = isolate::create_context(&mut iso);
6918 let (code, _, _) = {
6919 let scope = &mut v8::HandleScope::new(&mut iso);
6920 let local = v8::Local::new(scope, &ctx);
6921 let scope = &mut v8::ContextScope::new(scope, local);
6922 execute_module(
6923 scope,
6924 &bridge_ctx,
6925 bridge,
6926 "export const a = 1;",
6927 None,
6928 &mut cache,
6929 )
6930 };
6931 assert_eq!(code, 0);
6932 assert!(cache.is_some());
6933 }
6934
6935 {
6937 let ctx = isolate::create_context(&mut iso);
6938 let (code, exports, _) = {
6939 let scope = &mut v8::HandleScope::new(&mut iso);
6940 let local = v8::Local::new(scope, &ctx);
6941 let scope = &mut v8::ContextScope::new(scope, local);
6942 execute_module(
6943 scope,
6944 &bridge_ctx,
6945 bridge,
6946 "export const b = 2;",
6947 None,
6948 &mut cache,
6949 )
6950 };
6951 assert_eq!(code, 0);
6952 assert!(exports.is_some());
6953 assert!(cache.is_some());
6954 }
6955 }
6956
6957 {
6959 let mut iso = isolate::create_isolate(None);
6960 let ctx = isolate::create_context(&mut iso);
6961 let mut cache: Option<BridgeCodeCache> = None;
6962
6963 let (code, _) = {
6964 let scope = &mut v8::HandleScope::new(&mut iso);
6965 let local = v8::Local::new(scope, &ctx);
6966 let scope = &mut v8::ContextScope::new(scope, local);
6967 execute_script(scope, "", "var x = 1;", &mut cache)
6968 };
6969
6970 assert_eq!(code, 0);
6971 assert!(
6972 cache.is_none(),
6973 "cache should not be populated for empty bridge code"
6974 );
6975 }
6976
6977 {
6979 let mut iso = isolate::create_isolate(None);
6980 let ctx = isolate::create_context(&mut iso);
6981
6982 let mut response_buf = Vec::new();
6983
6984 let batch_result = v8_serialize_eval(
6986 &mut iso,
6987 &ctx,
6988 "[{resolved: '/a.mjs', source: 'export const a = 1;'}, {resolved: '/b.mjs', source: 'export const b = 2;'}]",
6989 );
6990 crate::ipc_binary::write_frame(
6991 &mut response_buf,
6992 &crate::ipc_binary::BinaryFrame::BridgeResponse {
6993 session_id: String::new(),
6994 call_id: 1,
6995 status: 0,
6996 payload: batch_result,
6997 },
6998 )
6999 .unwrap();
7000 crate::ipc_binary::write_frame(
7001 &mut response_buf,
7002 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7003 session_id: String::new(),
7004 call_id: 2,
7005 status: 0,
7006 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7007 },
7008 )
7009 .unwrap();
7010 crate::ipc_binary::write_frame(
7011 &mut response_buf,
7012 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7013 session_id: String::new(),
7014 call_id: 3,
7015 status: 0,
7016 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7017 },
7018 )
7019 .unwrap();
7020
7021 let writer_buf = Arc::new(Mutex::new(Vec::new()));
7022 let bridge_ctx = BridgeCallContext::new(
7023 Box::new(SharedWriter(Arc::clone(&writer_buf))),
7024 Box::new(Cursor::new(response_buf)),
7025 "test-session".into(),
7026 );
7027
7028 let user_code = "import { a } from './a.mjs';\nimport { b } from './b.mjs';\nexport const sum = a + b;";
7029 let (code, exports, error) = {
7030 let scope = &mut v8::HandleScope::new(&mut iso);
7031 let local = v8::Local::new(scope, &ctx);
7032 let scope = &mut v8::ContextScope::new(scope, local);
7033 execute_module(
7034 scope,
7035 &bridge_ctx,
7036 "",
7037 user_code,
7038 Some("/app/main.mjs"),
7039 &mut None,
7040 )
7041 };
7042
7043 assert_eq!(code, 0, "error: {:?}", error);
7044 assert!(error.is_none());
7045 let exports = exports.unwrap();
7046 {
7047 let scope = &mut v8::HandleScope::new(&mut iso);
7048 let local = v8::Local::new(scope, &ctx);
7049 let scope = &mut v8::ContextScope::new(scope, local);
7050 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7051 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7052 let k = v8::String::new(scope, "sum").unwrap();
7053 assert_eq!(
7054 obj.get(scope, k.into())
7055 .unwrap()
7056 .int32_value(scope)
7057 .unwrap(),
7058 3
7059 );
7060 }
7061
7062 let written = writer_buf.lock().unwrap();
7064 let call = crate::ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
7065 match call {
7066 crate::ipc_binary::BinaryFrame::BridgeCall { method, .. } => {
7067 assert_eq!(method, "_batchResolveModules");
7068 }
7069 _ => panic!("expected BridgeCall for _batchResolveModules"),
7070 }
7071 }
7072
7073 {
7075 let mut iso = isolate::create_isolate(None);
7076 let ctx = isolate::create_context(&mut iso);
7077
7078 let mut response_buf = Vec::new();
7079
7080 crate::ipc_binary::write_frame(
7082 &mut response_buf,
7083 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7084 session_id: String::new(),
7085 call_id: 1,
7086 status: 1,
7087 payload: "No handler for bridge method: _batchResolveModules"
7088 .as_bytes()
7089 .to_vec(),
7090 },
7091 )
7092 .unwrap();
7093
7094 let resolve_result = v8_serialize_str(&mut iso, &ctx, "/dep.mjs");
7096 crate::ipc_binary::write_frame(
7097 &mut response_buf,
7098 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7099 session_id: String::new(),
7100 call_id: 2,
7101 status: 0,
7102 payload: resolve_result,
7103 },
7104 )
7105 .unwrap();
7106
7107 let load_result = v8_serialize_str(&mut iso, &ctx, "export const val = 42;");
7109 crate::ipc_binary::write_frame(
7110 &mut response_buf,
7111 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7112 session_id: String::new(),
7113 call_id: 3,
7114 status: 0,
7115 payload: load_result,
7116 },
7117 )
7118 .unwrap();
7119 crate::ipc_binary::write_frame(
7120 &mut response_buf,
7121 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7122 session_id: String::new(),
7123 call_id: 4,
7124 status: 0,
7125 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7126 },
7127 )
7128 .unwrap();
7129
7130 let bridge_ctx = BridgeCallContext::new(
7131 Box::new(Vec::new()),
7132 Box::new(Cursor::new(response_buf)),
7133 "test-session".into(),
7134 );
7135
7136 let user_code = "import { val } from './dep.mjs';\nexport const result = val;";
7137 let (code, exports, error) = {
7138 let scope = &mut v8::HandleScope::new(&mut iso);
7139 let local = v8::Local::new(scope, &ctx);
7140 let scope = &mut v8::ContextScope::new(scope, local);
7141 execute_module(
7142 scope,
7143 &bridge_ctx,
7144 "",
7145 user_code,
7146 Some("/app/main.mjs"),
7147 &mut None,
7148 )
7149 };
7150
7151 assert_eq!(code, 0, "error: {:?}", error);
7152 assert!(error.is_none());
7153 let exports = exports.unwrap();
7154 {
7155 let scope = &mut v8::HandleScope::new(&mut iso);
7156 let local = v8::Local::new(scope, &ctx);
7157 let scope = &mut v8::ContextScope::new(scope, local);
7158 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7159 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7160 let k = v8::String::new(scope, "result").unwrap();
7161 assert_eq!(
7162 obj.get(scope, k.into())
7163 .unwrap()
7164 .int32_value(scope)
7165 .unwrap(),
7166 42
7167 );
7168 }
7169 }
7170
7171 {
7173 let mut iso = isolate::create_isolate(None);
7174 let ctx = isolate::create_context(&mut iso);
7175
7176 let mut response_buf = Vec::new();
7177
7178 let batch1 = v8_serialize_eval(
7180 &mut iso,
7181 &ctx,
7182 "[{resolved: '/a.mjs', source: \"import { b } from './b.mjs'; export const a = b + 1;\"}]",
7183 );
7184 crate::ipc_binary::write_frame(
7185 &mut response_buf,
7186 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7187 session_id: String::new(),
7188 call_id: 1,
7189 status: 0,
7190 payload: batch1,
7191 },
7192 )
7193 .unwrap();
7194 crate::ipc_binary::write_frame(
7195 &mut response_buf,
7196 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7197 session_id: String::new(),
7198 call_id: 2,
7199 status: 0,
7200 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7201 },
7202 )
7203 .unwrap();
7204
7205 let batch2 = v8_serialize_eval(
7207 &mut iso,
7208 &ctx,
7209 "[{resolved: '/b.mjs', source: 'export const b = 10;'}]",
7210 );
7211 crate::ipc_binary::write_frame(
7212 &mut response_buf,
7213 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7214 session_id: String::new(),
7215 call_id: 3,
7216 status: 0,
7217 payload: batch2,
7218 },
7219 )
7220 .unwrap();
7221 crate::ipc_binary::write_frame(
7222 &mut response_buf,
7223 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7224 session_id: String::new(),
7225 call_id: 4,
7226 status: 0,
7227 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7228 },
7229 )
7230 .unwrap();
7231
7232 let bridge_ctx = BridgeCallContext::new(
7233 Box::new(Vec::new()),
7234 Box::new(Cursor::new(response_buf)),
7235 "test-session".into(),
7236 );
7237
7238 let user_code = "import { a } from './a.mjs';\nexport const result = a;";
7239 let (code, exports, error) = {
7240 let scope = &mut v8::HandleScope::new(&mut iso);
7241 let local = v8::Local::new(scope, &ctx);
7242 let scope = &mut v8::ContextScope::new(scope, local);
7243 execute_module(
7244 scope,
7245 &bridge_ctx,
7246 "",
7247 user_code,
7248 Some("/app/main.mjs"),
7249 &mut None,
7250 )
7251 };
7252
7253 assert_eq!(code, 0, "error: {:?}", error);
7254 assert!(error.is_none());
7255 let exports = exports.unwrap();
7256 {
7257 let scope = &mut v8::HandleScope::new(&mut iso);
7258 let local = v8::Local::new(scope, &ctx);
7259 let scope = &mut v8::ContextScope::new(scope, local);
7260 let val = crate::bridge::deserialize_v8_value(scope, &exports).unwrap();
7261 let obj = v8::Local::<v8::Object>::try_from(val).unwrap();
7262 let k = v8::String::new(scope, "result").unwrap();
7263 assert_eq!(
7264 obj.get(scope, k.into())
7265 .unwrap()
7266 .int32_value(scope)
7267 .unwrap(),
7268 11
7269 );
7270 }
7271 }
7272
7273 {
7275 let mut iso = isolate::create_isolate(None);
7276 let ctx = isolate::create_context(&mut iso);
7277
7278 let writer_buf = Arc::new(Mutex::new(Vec::new()));
7279 let bridge_ctx = BridgeCallContext::new(
7280 Box::new(SharedWriter(Arc::clone(&writer_buf))),
7281 Box::new(Cursor::new(Vec::new())),
7282 "test-session".into(),
7283 );
7284
7285 let user_code = "export const x = 42;";
7286 let (code, _exports, error) = {
7287 let scope = &mut v8::HandleScope::new(&mut iso);
7288 let local = v8::Local::new(scope, &ctx);
7289 let scope = &mut v8::ContextScope::new(scope, local);
7290 execute_module(scope, &bridge_ctx, "", user_code, None, &mut None)
7291 };
7292
7293 assert_eq!(code, 0, "error: {:?}", error);
7294 assert!(error.is_none());
7295
7296 let written = writer_buf.lock().unwrap();
7298 assert!(
7299 written.is_empty(),
7300 "no IPC calls expected for module with no imports"
7301 );
7302 }
7303
7304 {
7306 let mut iso = isolate::create_isolate(None);
7307 let ctx = isolate::create_context(&mut iso);
7308 let scope = &mut v8::HandleScope::new(&mut iso);
7309 let local = v8::Local::new(scope, &ctx);
7310 let scope = &mut v8::ContextScope::new(scope, local);
7311
7312 let mut source_code = String::new();
7313 for i in 0..(MAX_MODULE_PREFETCH_BATCH_SIZE + 1) {
7314 source_code.push_str(&format!("import './dep-{i}.mjs';\n"));
7315 }
7316 source_code.push_str("export const ok = true;");
7317
7318 let resource = v8::String::new(scope, "/app/main.mjs").unwrap();
7319 let origin = v8::ScriptOrigin::new(
7320 scope,
7321 resource.into(),
7322 0,
7323 0,
7324 false,
7325 -1,
7326 None,
7327 false,
7328 false,
7329 true,
7330 None,
7331 );
7332 let source = v8::String::new(scope, &source_code).unwrap();
7333 let mut compiled = v8::script_compiler::Source::new(source, Some(&origin));
7334 let module = v8::script_compiler::compile_module(scope, &mut compiled).unwrap();
7335
7336 MODULE_RESOLVE_STATE.with(|cell| {
7337 *cell.borrow_mut() = Some(ModuleResolveState {
7338 bridge_ctx: std::ptr::null(),
7339 module_names: HashMap::new(),
7340 module_cache: HashMap::new(),
7341 guest_reader: None,
7342 });
7343 });
7344 let imports = extract_uncached_imports(scope, module, "/app/main.mjs");
7345 assert_eq!(
7346 imports.len(),
7347 MAX_MODULE_PREFETCH_BATCH_SIZE,
7348 "static import extraction should stop at the prefetch batch cap"
7349 );
7350 clear_module_state();
7351 }
7352
7353 {
7355 let mut iso = isolate::create_isolate(None);
7356 let ctx = isolate::create_context(&mut iso);
7357 let scope = &mut v8::HandleScope::new(&mut iso);
7358 let local = v8::Local::new(scope, &ctx);
7359 let scope = &mut v8::ContextScope::new(scope, local);
7360
7361 let resource = v8::String::new(scope, "/overflow.mjs").unwrap();
7362 let origin = v8::ScriptOrigin::new(
7363 scope,
7364 resource.into(),
7365 0,
7366 0,
7367 false,
7368 -1,
7369 None,
7370 false,
7371 false,
7372 true,
7373 None,
7374 );
7375 let source = v8::String::new(scope, "export const value = 1;").unwrap();
7376 let mut compiled = v8::script_compiler::Source::new(source, Some(&origin));
7377 let module = v8::script_compiler::compile_module(scope, &mut compiled).unwrap();
7378 let global = v8::Global::new(scope, module);
7379
7380 let mut module_cache = HashMap::new();
7381 for i in 0..(MAX_MODULE_RESOLVE_CACHE_ENTRIES - 1) {
7382 module_cache.insert(format!("/cached-{i}.mjs"), global.clone());
7383 }
7384 MODULE_RESOLVE_STATE.with(|cell| {
7385 *cell.borrow_mut() = Some(ModuleResolveState {
7386 bridge_ctx: std::ptr::null(),
7387 module_names: HashMap::new(),
7388 module_cache,
7389 guest_reader: None,
7390 });
7391 });
7392
7393 assert!(
7394 !cache_resolved_module(
7395 module,
7396 global,
7397 "/overflow.mjs".into(),
7398 Some(module_request_cache_key("./overflow.mjs", "/app/main.mjs")),
7399 ),
7400 "cache insert should fail instead of exceeding the cache entry cap"
7401 );
7402 let cache_len = MODULE_RESOLVE_STATE.with(|cell| {
7403 cell.borrow()
7404 .as_ref()
7405 .expect("module state")
7406 .module_cache
7407 .len()
7408 });
7409 assert_eq!(
7410 cache_len,
7411 MAX_MODULE_RESOLVE_CACHE_ENTRIES - 1,
7412 "failed cache insert must not partially insert entries"
7413 );
7414 clear_module_state();
7415 }
7416
7417 {
7419 let mut iso = isolate::create_isolate(None);
7420 let ctx = isolate::create_context(&mut iso);
7421
7422 let oversized_response = v8_serialize_eval(
7423 &mut iso,
7424 &ctx,
7425 "[{resolved: '/a.mjs', source: 'export const a = 1;'}, {resolved: '/extra.mjs', source: 'export const extra = 1;'}]",
7426 );
7427 let mut response_buf = Vec::new();
7428 crate::ipc_binary::write_frame(
7429 &mut response_buf,
7430 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7431 session_id: String::new(),
7432 call_id: 1,
7433 status: 0,
7434 payload: oversized_response,
7435 },
7436 )
7437 .unwrap();
7438 let bridge_ctx = BridgeCallContext::new(
7439 Box::new(Vec::new()),
7440 Box::new(Cursor::new(response_buf)),
7441 "test-session".into(),
7442 );
7443
7444 let results = {
7445 let scope = &mut v8::HandleScope::new(&mut iso);
7446 let local = v8::Local::new(scope, &ctx);
7447 let scope = &mut v8::ContextScope::new(scope, local);
7448 batch_resolve_via_ipc(
7449 scope,
7450 &bridge_ctx,
7451 &[("./a.mjs".to_string(), "/app/main.mjs".to_string())],
7452 )
7453 .expect("batch resolve response")
7454 };
7455 assert_eq!(
7456 results.len(),
7457 1,
7458 "batch response parser must not retain entries beyond the request length"
7459 );
7460 assert_eq!(
7461 results[0]
7462 .as_ref()
7463 .map(|(resolved, _source)| resolved.as_str()),
7464 Some("/a.mjs")
7465 );
7466
7467 let mut capped_response_buf = Vec::new();
7468 crate::ipc_binary::write_frame(
7469 &mut capped_response_buf,
7470 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7471 session_id: String::new(),
7472 call_id: 1,
7473 status: 0,
7474 payload: vec![0; MAX_MODULE_BATCH_RESOLVE_RESPONSE_BYTES + 1],
7475 },
7476 )
7477 .unwrap();
7478 let capped_bridge_ctx = BridgeCallContext::new(
7479 Box::new(Vec::new()),
7480 Box::new(Cursor::new(capped_response_buf)),
7481 "test-session".into(),
7482 );
7483 let capped_result = {
7484 let scope = &mut v8::HandleScope::new(&mut iso);
7485 let local = v8::Local::new(scope, &ctx);
7486 let scope = &mut v8::ContextScope::new(scope, local);
7487 batch_resolve_via_ipc(
7488 scope,
7489 &capped_bridge_ctx,
7490 &[("./large.mjs".to_string(), "/app/main.mjs".to_string())],
7491 )
7492 };
7493 assert!(
7494 capped_result.is_none(),
7495 "batch response payloads over the byte cap should be rejected before deserialization"
7496 );
7497 }
7498
7499 {
7501 let mut source = String::new();
7502 for i in 0..(MAX_CJS_NAMED_EXPORTS + 1) {
7503 source.push_str(&format!("exports.name{i} = {i};\n"));
7504 }
7505
7506 let exports = extract_cjs_export_names(&source);
7507 assert_eq!(
7508 exports.len(),
7509 MAX_CJS_NAMED_EXPORTS,
7510 "static CJS export extraction should stop at the named export cap"
7511 );
7512 assert!(
7513 !exports.contains(&format!("name{}", MAX_CJS_NAMED_EXPORTS)),
7514 "exports beyond the cap must not be retained"
7515 );
7516
7517 let object_literal_exports =
7518 extract_cjs_export_names("module.exports = { foo: 1, shorthand, default: 2 };");
7519 assert!(
7520 object_literal_exports.contains(&"foo".to_string()),
7521 "module.exports object literal keys should be statically extracted"
7522 );
7523 assert!(
7524 object_literal_exports.contains(&"shorthand".to_string()),
7525 "module.exports shorthand keys should be statically extracted"
7526 );
7527 assert!(
7528 !object_literal_exports.contains(&"default".to_string()),
7529 "default should not be emitted as a named CJS export"
7530 );
7531
7532 let object_assign_exports =
7533 extract_cjs_export_names("Object.assign(module.exports, { bar: 1, baz });");
7534 assert!(
7535 object_assign_exports.contains(&"bar".to_string())
7536 && object_assign_exports.contains(&"baz".to_string()),
7537 "Object.assign(module.exports, object literal) keys should be extracted"
7538 );
7539
7540 let multiline_exports = extract_cjs_export_names(
7541 r#"
7542 module.exports = {
7543 multiFoo: 1,
7544 multiBar,
7545 };
7546
7547 Object.assign(module.exports, {
7548 multiBaz: 2,
7549 });
7550 "#,
7551 );
7552 assert!(
7553 multiline_exports.contains(&"multiFoo".to_string())
7554 && multiline_exports.contains(&"multiBar".to_string())
7555 && multiline_exports.contains(&"multiBaz".to_string()),
7556 "multiline CJS object literal export keys should be extracted"
7557 );
7558
7559 let false_positive_exports = extract_cjs_export_names(
7560 r#"
7561 module.exports.foo = { fakeOne: 1 };
7562 Object.assign(otherTarget, { fakeTwo: 2 });
7563 // module.exports = { fakeThree: 3 };
7564 const text = "Object.assign(module.exports, { fakeFour: 4 })";
7565 /* exports.fakeFive = 5; */
7566 const tpl = `Object.defineProperty(exports, "fakeSix", {})`;
7567 module.exports = { "fake:seven": 7 };
7568 const re = /module.exports = { fakeEight: 8 }/;
7569 function f() { return /module.exports = { fakeNine: 9 }/; }
7570 const g = () => /exports.fakeTen = 10/;
7571 const h = /[/]module.exports = { fakeEleven: 11 }/;
7572 if (ok) /exports.fakeTwelve = 12/.test(input);
7573 if (ok) {} /exports.fakeThirteen = 13/.test(input);
7574 "#,
7575 );
7576 assert!(
7577 !false_positive_exports.contains(&"fakeOne".to_string())
7578 && !false_positive_exports.contains(&"fakeTwo".to_string())
7579 && !false_positive_exports.contains(&"fakeThree".to_string())
7580 && !false_positive_exports.contains(&"fakeFour".to_string())
7581 && !false_positive_exports.contains(&"fakeFive".to_string())
7582 && !false_positive_exports.contains(&"fakeSix".to_string())
7583 && !false_positive_exports.contains(&"fake".to_string())
7584 && !false_positive_exports.contains(&"fakeEight".to_string())
7585 && !false_positive_exports.contains(&"fakeNine".to_string())
7586 && !false_positive_exports.contains(&"fakeTen".to_string())
7587 && !false_positive_exports.contains(&"fakeEleven".to_string())
7588 && !false_positive_exports.contains(&"fakeTwelve".to_string())
7589 && !false_positive_exports.contains(&"fakeThirteen".to_string()),
7590 "object literal extraction should not emit keys from unrelated objects"
7591 );
7592
7593 let mut malformed_literals = String::new();
7594 for i in 0..2048 {
7595 malformed_literals.push_str(&format!("module.exports = {{ fake{i}: "));
7596 }
7597 let malformed_exports = extract_cjs_export_names(&malformed_literals);
7598 assert!(
7599 malformed_exports.is_empty(),
7600 "malformed object literals should be skipped without collecting fake keys"
7601 );
7602
7603 let regex_value_exports =
7604 extract_cjs_export_names("module.exports = { real: /}/, alsoReal: /[,]}/ };");
7605 assert!(
7606 regex_value_exports.contains(&"real".to_string())
7607 && regex_value_exports.contains(&"alsoReal".to_string()),
7608 "regex values inside CJS object literals should not terminate the object scan"
7609 );
7610
7611 let division_exports = extract_cjs_export_names("const n = 4 / 2; exports.after = n;");
7612 assert!(
7613 division_exports.contains(&"after".to_string()),
7614 "ordinary division should not hide later CJS export assignments"
7615 );
7616
7617 let reserved_exports = extract_cjs_export_names(
7618 r#"
7619 exports.arguments = 1;
7620 exports.class = 1;
7621 module.exports = { await: 2 };
7622 module.exports = { let: 3, static: 4, eval: 5 };
7623 Object.assign(module.exports, {
7624 implements: 6,
7625 interface: 7,
7626 package: 8,
7627 private: 9,
7628 protected: 10,
7629 public: 11,
7630 });
7631 Object.defineProperty(exports, "return", {});
7632 "#,
7633 );
7634 assert!(
7635 reserved_exports.is_empty(),
7636 "reserved words should not be emitted as generated ESM bindings"
7637 );
7638
7639 let mut huge_literal = String::from("module.exports = {\n");
7640 for i in 0..(MAX_CJS_NAMED_EXPORTS + 1) {
7641 huge_literal.push_str(&format!("literalName{i}: {i},\n"));
7642 }
7643 huge_literal.push_str("};");
7644 let huge_literal_exports = extract_cjs_export_names(&huge_literal);
7645 assert_eq!(
7646 huge_literal_exports.len(),
7647 MAX_CJS_NAMED_EXPORTS,
7648 "object literal export extraction should stop at the named export cap"
7649 );
7650 assert!(
7651 !huge_literal_exports.contains(&format!("literalName{}", MAX_CJS_NAMED_EXPORTS)),
7652 "object literal exports beyond the cap must not be retained"
7653 );
7654
7655 let mut iso = isolate::create_isolate(None);
7656 let ctx = isolate::create_context(&mut iso);
7657 let scope = &mut v8::HandleScope::new(&mut iso);
7658 let local = v8::Local::new(scope, &ctx);
7659 let scope = &mut v8::ContextScope::new(scope, local);
7660 let shim =
7661 build_cjs_esm_shim(scope, "module.exports = { foo: 1 };", "/object-literal.cjs");
7662 assert!(
7663 shim.contains("export const foo = _cjsModule[\"foo\"];"),
7664 "CJS shim should preserve statically extractable named exports"
7665 );
7666 }
7667
7668 {
7670 let mut iso = isolate::create_isolate(None);
7671 let ctx = isolate::create_context(&mut iso);
7672 let scope = &mut v8::HandleScope::new(&mut iso);
7673 let local = v8::Local::new(scope, &ctx);
7674 let scope = &mut v8::ContextScope::new(scope, local);
7675
7676 let shim = build_cjs_esm_shim(
7677 scope,
7678 "module.exports = makeExportsDynamically();",
7679 "/runtime.cjs",
7680 );
7681
7682 assert!(
7683 shim.contains("export default _cjsModule;"),
7684 "CJS shim should preserve default import support"
7685 );
7686 assert!(
7687 !shim.contains("export const name0"),
7688 "CJS shim must degrade to default-only when runtime extraction is unavailable"
7689 );
7690 }
7691
7692 {
7694 let mut iso = isolate::create_isolate(None);
7695 let ctx = isolate::create_context(&mut iso);
7696 let scope = &mut v8::HandleScope::new(&mut iso);
7697 let local = v8::Local::new(scope, &ctx);
7698 let scope = &mut v8::ContextScope::new(scope, local);
7699
7700 let setup = v8::String::new(
7701 scope,
7702 "globalThis._requireFrom = function (path, referrer) { return { dynamicA: 1, dynamicB: 2, default: 3, __esModule: true }; };",
7703 )
7704 .unwrap();
7705 let script = v8::Script::compile(scope, setup, None).unwrap();
7706 script.run(scope).unwrap();
7707
7708 let shim = build_cjs_esm_shim(
7709 scope,
7710 "module.exports = makeExportsDynamically();",
7711 "/dynamic.cjs",
7712 );
7713
7714 assert!(
7715 shim.contains("export const dynamicA = _cjsModule[\"dynamicA\"];"),
7716 "runtime fallback should surface dynamically computed named exports"
7717 );
7718 assert!(
7719 shim.contains("export const dynamicB = _cjsModule[\"dynamicB\"];"),
7720 "runtime fallback should surface every dynamically computed named export"
7721 );
7722 assert!(
7723 shim.contains("export default _cjsModule;"),
7724 "CJS shim should preserve default import support"
7725 );
7726 assert!(
7727 !shim.contains("export const default"),
7728 "runtime fallback must not emit a named export for default"
7729 );
7730 assert!(
7731 !shim.contains("__esModule"),
7732 "runtime fallback must not emit a named export for __esModule"
7733 );
7734 }
7735
7736 {
7738 let mut iso = isolate::create_isolate(None);
7739 let ctx = isolate::create_context(&mut iso);
7740 let scope = &mut v8::HandleScope::new(&mut iso);
7741 let local = v8::Local::new(scope, &ctx);
7742 let scope = &mut v8::ContextScope::new(scope, local);
7743
7744 let setup = v8::String::new(
7745 scope,
7746 "globalThis._requireFrom = function () { const o = {}; for (let i = 0; i < 1025; i++) o[\"k\" + String(i).padStart(4, \"0\")] = i; o[\"x\".repeat(600)] = 1; return o; };",
7747 )
7748 .unwrap();
7749 let script = v8::Script::compile(scope, setup, None).unwrap();
7750 script.run(scope).unwrap();
7751
7752 let shim = build_cjs_esm_shim(
7753 scope,
7754 "module.exports = makeExportsDynamically();",
7755 "/bounded.cjs",
7756 );
7757
7758 let export_count = shim.matches("export const ").count();
7759 assert_eq!(
7760 export_count, MAX_CJS_NAMED_EXPORTS,
7761 "runtime fallback should stop collecting names at the named export cap"
7762 );
7763 assert!(
7764 !shim.contains("export const k1024"),
7765 "runtime fallback exports beyond the cap must not be retained"
7766 );
7767 let longest_export_name = shim
7768 .lines()
7769 .filter_map(|line| line.strip_prefix("export const "))
7770 .filter_map(|rest| rest.split(' ').next())
7771 .map(str::len)
7772 .max()
7773 .unwrap_or(0);
7774 assert!(
7775 longest_export_name <= MAX_CJS_RUNTIME_EXPORT_NAME_LEN,
7776 "runtime fallback must skip export names longer than the length cap"
7777 );
7778 }
7779
7780 {
7782 let mut iso = isolate::create_isolate(None);
7783 let ctx = isolate::create_context(&mut iso);
7784 let scope = &mut v8::HandleScope::new(&mut iso);
7785 let local = v8::Local::new(scope, &ctx);
7786 let scope = &mut v8::ContextScope::new(scope, local);
7787
7788 let setup = v8::String::new(
7789 scope,
7790 "globalThis._requireFrom = function () { throw new Error(\"boom\"); };",
7791 )
7792 .unwrap();
7793 let script = v8::Script::compile(scope, setup, None).unwrap();
7794 script.run(scope).unwrap();
7795
7796 let shim = build_cjs_esm_shim(
7797 scope,
7798 "module.exports = makeExportsDynamically();",
7799 "/throwing.cjs",
7800 );
7801
7802 assert!(
7803 shim.contains("export default _cjsModule;"),
7804 "CJS shim should preserve default import support after a guest throw"
7805 );
7806 assert!(
7807 !shim.contains("export const "),
7808 "runtime fallback should yield no named exports when module evaluation throws"
7809 );
7810 }
7811
7812 {
7814 let mut iso = isolate::create_isolate(None);
7815 iso.set_host_import_module_dynamically_callback(dynamic_import_callback);
7816 iso.set_host_initialize_import_meta_object_callback(import_meta_object_callback);
7817 let ctx = isolate::create_context(&mut iso);
7818
7819 let mut response_buf = Vec::new();
7820
7821 let resolve_result = v8_serialize_str(&mut iso, &ctx, "/dep.mjs");
7822 crate::ipc_binary::write_frame(
7823 &mut response_buf,
7824 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7825 session_id: String::new(),
7826 call_id: 1,
7827 status: 0,
7828 payload: resolve_result,
7829 },
7830 )
7831 .unwrap();
7832
7833 let load_result = v8_serialize_str(&mut iso, &ctx, "export const value = 42;");
7834 crate::ipc_binary::write_frame(
7835 &mut response_buf,
7836 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7837 session_id: String::new(),
7838 call_id: 2,
7839 status: 0,
7840 payload: load_result,
7841 },
7842 )
7843 .unwrap();
7844 crate::ipc_binary::write_frame(
7845 &mut response_buf,
7846 &crate::ipc_binary::BinaryFrame::BridgeResponse {
7847 session_id: String::new(),
7848 call_id: 3,
7849 status: 0,
7850 payload: v8_serialize_str(&mut iso, &ctx, "module"),
7851 },
7852 )
7853 .unwrap();
7854
7855 let bridge_ctx = BridgeCallContext::new(
7856 Box::new(Vec::new()),
7857 Box::new(Cursor::new(response_buf)),
7858 "test-session".into(),
7859 );
7860
7861 let user_code = r#"
7862 globalThis.loadDep = async () => (await import("./dep.mjs")).value;
7863 export const ready = true;
7864 "#;
7865 let (code, exports, error) = {
7866 let scope = &mut v8::HandleScope::new(&mut iso);
7867 let local = v8::Local::new(scope, &ctx);
7868 let scope = &mut v8::ContextScope::new(scope, local);
7869 execute_module(
7870 scope,
7871 &bridge_ctx,
7872 "",
7873 user_code,
7874 Some("/app/main.mjs"),
7875 &mut None,
7876 )
7877 };
7878
7879 assert_eq!(code, 0, "error: {:?}", error);
7880 assert!(error.is_none());
7881 assert!(exports.is_some());
7882
7883 {
7884 let scope = &mut v8::HandleScope::new(&mut iso);
7885 let local = v8::Local::new(scope, &ctx);
7886 let scope = &mut v8::ContextScope::new(scope, local);
7887 let tc = &mut v8::TryCatch::new(scope);
7888 let source = v8::String::new(
7889 tc,
7890 "globalThis.__depPromise = globalThis.loadDep().then((value) => { globalThis.__depValue = value; return value; });",
7891 )
7892 .unwrap();
7893 let script = v8::Script::compile(tc, source, None).unwrap();
7894 assert!(script.run(tc).is_some());
7895 tc.perform_microtask_checkpoint();
7896 assert!(tc.exception().is_none());
7897 }
7898
7899 assert_eq!(eval(&mut iso, &ctx, "String(globalThis.__depValue)"), "42");
7900 clear_module_state();
7901 }
7902 }
7903}