use bao_engine::context::JsContext;
use bao_engine::value::JsValue;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn write_module(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&path, content).unwrap();
path
}
fn jsvalue_to_string(v: &JsValue) -> String {
match v {
JsValue::String(s) => s.clone(),
JsValue::Number(n) => format!("{}", n),
JsValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
JsValue::Null => "null".to_string(),
JsValue::Undefined => "undefined".to_string(),
_ => "<object>".to_string(),
}
}
fn eval_string(ctx: &mut JsContext, source: &str, filename: &str) -> String {
match ctx.eval(source, filename) {
Ok(v) => jsvalue_to_string(&v),
Err(e) => format!("ERR:{}", e.message),
}
}
fn eval_module(ctx: &mut JsContext, source: &str, filename: &str) -> String {
use bao_engine::module_loader::ModuleLoader;
let setup = ctx.global_setup();
let hook = ctx.post_eval_hook();
let mut cx = ctx.cx();
match ModuleLoader::eval_module(&mut cx, source, filename, setup, hook) {
Ok(v) => jsvalue_to_string(&v),
Err(e) => format!("ERR:{}", e.message),
}
}
fn eval_module_ok(ctx: &mut JsContext, source: &str, filename: &str) -> Result<(), String> {
use bao_engine::module_loader::ModuleLoader;
let setup = ctx.global_setup();
let hook = ctx.post_eval_hook();
let mut cx = ctx.cx();
ModuleLoader::eval_module(&mut cx, source, filename, setup, hook)
.map(|_| ())
.map_err(|e| e.message)
}
#[test]
fn test_set_module_private_import_meta_url() {
let dir = TempDir::new().unwrap();
let module_path = write_module(
&dir,
"meta_test.mjs",
"export const url = import.meta.url;\nexport default import.meta.url;",
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let src = format!(
"const ns = require('{}');\nns.url.startsWith('file://') && ns.url.indexOf('meta_test.mjs') >= 0 ? 'PASS' : 'FAIL:' + ns.url",
module_path.to_string_lossy().replace('\\', "\\\\")
);
let result = eval_string(&mut ctx, &src, "<test>");
assert!(
result.starts_with("PASS"),
"import.meta.url should be a file:// URL, got: {}",
result
);
}
#[test]
fn test_module_private_relative_import_resolution() {
let dir = TempDir::new().unwrap();
let _ = write_module(&dir, "util.mjs", "export const VALUE = 42;");
let entry_path = write_module(
&dir,
"main.mjs",
"import { VALUE } from './util.mjs';\nexport default VALUE;\nexport { VALUE };",
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let src = format!(
"const ns = require('{}');\nns.VALUE === 42 ? 'PASS' : 'FAIL:' + ns.VALUE",
entry_path.to_string_lossy().replace('\\', "\\\\")
);
let result = eval_string(&mut ctx, &src, "<test>");
assert!(
result.starts_with("PASS"),
"relative import should resolve against module's dir: {}",
result
);
}
#[test]
fn test_finish_dynamic_module_import_file_module() {
let dir = TempDir::new().unwrap();
let entry_path = write_module(
&dir,
"dyn_dep.mjs",
"export const V = 7;\nexport default 7;",
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let src = format!(
"const ns = require('{}');\nns.default === 7 && ns.V === 7 ? 'PASS' : 'FAIL:' + ns.default + '/' + ns.V",
entry_path.to_string_lossy().replace('\\', "\\\\")
);
let result = eval_string(&mut ctx, &src, "<test>");
assert!(
result.starts_with("PASS"),
"dynamic import path module should resolve via FinishDynamicModuleImport: {}",
result
);
}
#[test]
fn test_require_of_esm_module() {
let dir = TempDir::new().unwrap();
let esm_path = write_module(
&dir,
"esm_lib.mjs",
"export const PI = 3.14;\nexport default 99;",
);
let entry_path = write_module(
&dir,
"cjs_main.js",
&format!(
"const ns = require('{}'); module.exports = ns;",
esm_path.to_string_lossy().replace('\\', "\\\\")
),
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let src = format!(
"const ns = require('{}'); ns.PI === 3.14 ? 'PASS' : 'FAIL:' + ns.PI",
esm_path.to_string_lossy().replace('\\', "\\\\")
);
let result = eval_string(&mut ctx, &src, &entry_path.to_string_lossy());
assert!(
result.starts_with("PASS"),
"require(esm) should expose named export: {}",
result
);
}
#[test]
fn test_esm_import_of_cjs_module() {
let dir = TempDir::new().unwrap();
let cjs_path = write_module(
&dir,
"cjs_lib.js",
"module.exports = { greet: function() { return 'hi'; }, VERSION: '1.0' };",
);
let _ = cjs_path;
let entry_path = write_module(
&dir,
"esm_main.mjs",
"import cjs from './cjs_lib.js';\nexport default cjs.VERSION + ':' + cjs.greet();",
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let result = eval_module(
&mut ctx,
"import main from './esm_main.mjs';\nexport default main;",
&entry_path.to_string_lossy(),
);
let _ = result; }
#[test]
fn test_top_level_await_basic() {
let dir = TempDir::new().unwrap();
let entry_path = write_module(
&dir,
"tla.mjs",
"const x = await Promise.resolve(42);\nexport default x;\nexport const V = x;",
);
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let src = format!(
"const ns = require('{}');\nns.V === 42 ? 'PASS' : 'FAIL:' + ns.V",
entry_path.to_string_lossy().replace('\\', "\\\\")
);
let result = eval_string(&mut ctx, &src, "<test>");
assert!(
result.starts_with("PASS"),
"top-level await module should resolve V=42 after RunJobs: {}",
result
);
}
#[test]
fn test_node_modules_resolution() {
let dir = TempDir::new().unwrap();
let nm_pkg = dir.path().join("node_modules").join("mylib");
fs::create_dir_all(&nm_pkg).unwrap();
fs::write(
nm_pkg.join("index.js"),
"module.exports = { from: 'node_modules' };",
)
.unwrap();
fs::write(
nm_pkg.join("package.json"),
r#"{"name": "mylib", "main": "index.js", "version": "1.0.0"}"#,
)
.unwrap();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let result = eval_string(
&mut ctx,
"const m = require('mylib'); m.from === 'node_modules' ? 'PASS' : 'FAIL:' + m.from",
"<test>",
);
assert!(
result.starts_with("PASS"),
"node_modules resolution: {}",
result
);
}
#[test]
fn test_node_modules_resolution_traverses_up() {
let dir = TempDir::new().unwrap();
let sub = dir.path().join("sub").join("deep");
fs::create_dir_all(&sub).unwrap();
let nm_pkg = dir.path().join("node_modules").join("upper_lib");
fs::create_dir_all(&nm_pkg).unwrap();
fs::write(nm_pkg.join("index.js"), "module.exports = 42;").unwrap();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(sub.clone());
let result = eval_string(
&mut ctx,
"const m = require('upper_lib'); m === 42 ? 'PASS' : 'FAIL:' + m",
"<test>",
);
assert!(
result.starts_with("PASS"),
"node_modules upward traversal: {}",
result
);
}
#[test]
fn test_require_resolve_returns_path() {
let dir = TempDir::new().unwrap();
let _target = write_module(&dir, "target.js", "module.exports = 1;");
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let result = eval_string(&mut ctx, "require.resolve('./target')", "<test>");
assert!(
result.contains("target.js"),
"require.resolve should return absolute path: {}",
result
);
}
#[test]
fn test_require_resolve_throws_on_missing() {
let dir = TempDir::new().unwrap();
let mut ctx = JsContext::for_test().expect("JsContext");
ctx.set_global_setup(bun_runtime::globals::install_all);
bun_runtime::require::set_require_dir(dir.path().to_path_buf());
let result = eval_string(
&mut ctx,
"try { require.resolve('./nonexistent'); 'NO_THROW'; } catch(e) { 'THREW:' + (e.message || e).toString().substring(0, 30); }",
"<test>",
);
assert!(
result.starts_with("THREW"),
"require.resolve should throw MODULE_NOT_FOUND: {}",
result
);
}
#[test]
fn test_cjs_detection_by_extension() {
use std::path::Path;
fn is_cjs(path: &Path, content: &str) -> bool {
match path.extension().and_then(|e| e.to_str()) {
Some("cjs") => return true,
Some("mjs") => return false,
_ => {}
}
let has_cjs_marker = content.contains("module.exports")
|| content.contains("exports.")
|| content.contains("exports[");
let has_esm_marker = content.contains("import ") || content.contains("export ");
if has_esm_marker && !has_cjs_marker {
return false;
}
has_cjs_marker
}
assert!(is_cjs(Path::new("a.cjs"), "anything"));
assert!(!is_cjs(Path::new("a.mjs"), "module.exports = 1;"));
assert!(is_cjs(Path::new("a.js"), "module.exports = {};"));
assert!(!is_cjs(Path::new("a.js"), "export default 1;"));
assert!(is_cjs(Path::new("a.js"), "exports.x = 1;"));
}
#[test]
fn test_percent_encode_decode_round_trip() {
fn encode(path: &str) -> String {
let mut out = String::with_capacity(path.len());
for b in path.bytes() {
let safe = b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~' | b'/');
if safe {
out.push(b as char);
} else {
out.push('%');
out.push_str(&format!("{:02X}", b));
}
}
out
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = hex_digit(bytes[i + 1]);
let lo = hex_digit(bytes[i + 2]);
if let (Some(h), Some(l)) = (hi, lo) {
out.push((h << 4) | l);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
let cases = [
"/home/user/file.js",
"/home/user with spaces/file.js",
"/tmp/中文目录/file.js",
"/var/log/app[1].js",
];
for case in cases {
let encoded = encode(case);
let decoded = decode(&encoded);
assert_eq!(decoded, case, "round-trip failed for: {}", case);
assert!(encoded.starts_with('/') || encoded.starts_with("%2F") || encoded.contains('/'));
}
}
#[test]
fn test_js_string_literal_escapes() {
fn render(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
assert_eq!(render("hello"), "\"hello\"");
assert_eq!(render("a\"b"), "\"a\\\"b\"");
assert_eq!(render("a\\b"), "\"a\\\\b\"");
assert_eq!(render("a\nb"), "\"a\\nb\"");
assert_eq!(render("a\tb"), "\"a\\tb\"");
assert_eq!(render("\u{1}"), "\"\\u0001\"");
}