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