use rquickjs::qjs;
use rquickjs::{CaughtError, Persistent, Value};
use std::ptr::NonNull;
#[rquickjs::module(rename = "camelCase")]
pub mod native_module {
use rquickjs::{Ctx, Value};
#[rquickjs::function]
pub fn eval_in_new_context<'js>(
ctx: Ctx<'js>,
code: String,
sandbox_keys: Vec<String>,
sandbox_values: Vec<Value<'js>>,
) -> rquickjs::Result<Value<'js>> {
super::eval_in_new_context_impl(ctx, &code, &sandbox_keys, &sandbox_values)
}
#[rquickjs::function]
pub fn eval_with_filename<'js>(
ctx: Ctx<'js>,
code: String,
filename: String,
) -> rquickjs::Result<Value<'js>> {
super::eval_with_filename_impl(ctx, &code, &filename)
}
#[rquickjs::function]
pub fn require_esm<'js>(ctx: Ctx<'js>, filename: String) -> rquickjs::Result<Value<'js>> {
super::require_esm_impl(ctx, &filename)
}
}
fn eval_in_new_context_impl<'js>(
caller_ctx: rquickjs::Ctx<'js>,
code: &str,
sandbox_keys: &[String],
sandbox_values: &[rquickjs::Value<'js>],
) -> rquickjs::Result<rquickjs::Value<'js>> {
let persistent_values: Vec<Persistent<Value<'static>>> = sandbox_values
.iter()
.map(|v| Persistent::save(&caller_ctx, v.clone()))
.collect();
let new_ctx: rquickjs::Ctx<'js> = unsafe {
let rt = qjs::JS_GetRuntime(caller_ctx.as_raw().as_ptr());
let raw_ctx = qjs::JS_NewContext(rt);
let nn = NonNull::new(raw_ctx).ok_or(rquickjs::Error::Unknown)?;
let ctx = rquickjs::Ctx::from_raw(nn);
qjs::JS_FreeContext(raw_ctx);
ctx
};
let new_global = new_ctx.globals();
for (key, pval) in sandbox_keys.iter().zip(persistent_values.into_iter()) {
let restored: Value<'js> = pval
.restore(&new_ctx)
.map_err(|_| rquickjs::Error::Unknown)?;
new_global.set(key.as_str(), restored)?;
}
let eval_result: Result<Value<'js>, _> = new_ctx.eval(code);
match eval_result {
Ok(result) => {
let persistent_result = Persistent::save(&new_ctx, result);
let caller_result: Value<'js> = persistent_result
.restore(&caller_ctx)
.map_err(|_| rquickjs::Error::Unknown)?;
Ok(caller_result)
}
Err(err) => {
let caught = CaughtError::catch(&new_ctx, Err::<(), _>(err));
if let Err(CaughtError::Exception(exc)) = caught {
let msg: String = exc
.message()
.unwrap_or_else(|| "Error in vm.runInNewContext".to_string());
let name: String = exc
.get::<_, rquickjs::String>("name")
.ok()
.and_then(|s| s.to_string().ok())
.unwrap_or_else(|| "Error".to_string());
let err_code = format!(
"(() => {{ throw new {}({}) }})()",
name,
serde_json_mini_quote(&msg),
);
let _: Result<Value<'js>, _> = caller_ctx.eval(err_code);
Err(rquickjs::Error::Exception)
} else if let Err(CaughtError::Value(val)) = caught {
let persistent_val = Persistent::save(&new_ctx, val);
if let Ok(restored) = persistent_val.restore(&caller_ctx) {
caller_ctx.throw(restored);
}
Err(rquickjs::Error::Exception)
} else {
Err(rquickjs::Error::Unknown)
}
}
}
}
fn require_esm_impl<'js>(
ctx: rquickjs::Ctx<'js>,
filename: &str,
) -> rquickjs::Result<rquickjs::Value<'js>> {
use std::ffi::CString;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let temp_key_str = format!("__wasm_rquickjs_require_esm_{}", id);
let wrapper_name = format!("<require-esm-{}>", id);
let file_url = if filename.starts_with("file://") {
filename.to_string()
} else if filename.starts_with('/') {
format!("file://{}", filename)
} else {
format!("file:///{}", filename)
};
let escaped_url = file_url
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n");
let code = format!(
"import * as __ns from \"{}\"; globalThis.{} = __ns;\n",
escaped_url, temp_key_str
);
let src = CString::new(code.as_str()).map_err(|_| rquickjs::Error::Unknown)?;
let fname = CString::new(wrapper_name.as_str()).map_err(|_| rquickjs::Error::Unknown)?;
let globals = ctx.globals();
unsafe {
let val = qjs::JS_Eval(
ctx.as_raw().as_ptr(),
src.as_ptr(),
code.len() as _,
fname.as_ptr(),
qjs::JS_EVAL_TYPE_MODULE as i32,
);
if qjs::JS_IsException(val) {
return Err(rquickjs::Error::Exception);
}
let tag = qjs::JS_VALUE_GET_TAG(val);
if tag == qjs::JS_TAG_OBJECT {
let catch_str = CString::new("catch").unwrap();
let catch_fn = qjs::JS_GetPropertyStr(ctx.as_raw().as_ptr(), val, catch_str.as_ptr());
if !qjs::JS_IsUndefined(catch_fn) && !qjs::JS_IsException(catch_fn) {
let noop_code = CString::new("(function(){})").unwrap();
let noop_fname = CString::new("<noop>").unwrap();
let noop_fn = qjs::JS_Eval(
ctx.as_raw().as_ptr(),
noop_code.as_ptr(),
14,
noop_fname.as_ptr(),
qjs::JS_EVAL_TYPE_GLOBAL as i32,
);
if !qjs::JS_IsException(noop_fn) {
let result = qjs::JS_Call(
ctx.as_raw().as_ptr(),
catch_fn,
val,
1,
&noop_fn as *const _ as *mut _,
);
if !qjs::JS_IsException(result) {
qjs::JS_FreeValue(ctx.as_raw().as_ptr(), result);
}
qjs::JS_FreeValue(ctx.as_raw().as_ptr(), noop_fn);
}
qjs::JS_FreeValue(ctx.as_raw().as_ptr(), catch_fn);
}
}
qjs::JS_FreeValue(ctx.as_raw().as_ptr(), val);
}
let ns: Value = globals.get(temp_key_str.as_str())?;
globals.remove(temp_key_str.as_str())?;
if ns.is_undefined() {
let error_ctor: rquickjs::Function = globals.get("Error")?;
let msg = format!(
"require() cannot be used on an ESM graph with top-level await. Use import() instead. Module: {}",
filename
);
let error_obj: rquickjs::Object = error_ctor.call((&msg,))?;
error_obj.set("code", "ERR_REQUIRE_ASYNC_MODULE")?;
Err(ctx.throw(error_obj.into_value()))
} else {
Ok(ns)
}
}
fn eval_with_filename_impl<'js>(
ctx: rquickjs::Ctx<'js>,
code: &str,
filename: &str,
) -> rquickjs::Result<rquickjs::Value<'js>> {
use std::ffi::CString;
let src = CString::new(code).map_err(|_| rquickjs::Error::Unknown)?;
let fname = CString::new(filename).map_err(|_| rquickjs::Error::Unknown)?;
let temp_key = c"__wasm_rquickjs_eval_tmp";
unsafe {
let val = qjs::JS_Eval(
ctx.as_raw().as_ptr(),
src.as_ptr(),
code.len() as _,
fname.as_ptr(),
qjs::JS_EVAL_TYPE_GLOBAL as i32,
);
if qjs::JS_IsException(val) {
return Err(rquickjs::Error::Exception);
}
let global = qjs::JS_GetGlobalObject(ctx.as_raw().as_ptr());
qjs::JS_SetPropertyStr(ctx.as_raw().as_ptr(), global, temp_key.as_ptr(), val);
qjs::JS_FreeValue(ctx.as_raw().as_ptr(), global);
}
let globals = ctx.globals();
let result: Value = globals.get("__wasm_rquickjs_eval_tmp")?;
globals.remove("__wasm_rquickjs_eval_tmp")?;
Ok(result)
}
fn serde_json_mini_quote(s: &str) -> String {
use std::fmt::Write;
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 < '\x20' => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
pub const VM_JS: &str = include_str!("vm.js");
pub const REEXPORT_JS: &str = r#"export * from '__wasm_rquickjs_builtin/vm'; export { default } from '__wasm_rquickjs_builtin/vm';"#;