use rquickjs::function::{Func, Opt, Rest, This};
use rquickjs::{Ctx, Function, Object, Promise, Result, Value};
use super::deep_equal::{Mode, deep_equal};
use super::inspect::{Inspector, MAX_DIR_DEPTH};
fn bind<'js>(ctx: &Ctx<'js>, target: &Function<'js>, args: Vec<Value<'js>>) -> Result<Function<'js>> {
let bind_fn: Function<'js> = target.get("bind")?;
let mut call_args = rquickjs::function::Args::new(ctx.clone(), args.len() + 1);
call_args.this(target.clone())?;
call_args.push_arg(Value::new_undefined(ctx.clone()))?;
for arg in args {
call_args.push_arg(arg)?;
}
call_args.apply(&bind_fn)
}
fn options_of<'js>(options: &Opt<Value<'js>>) -> Option<Object<'js>> {
options.0.as_ref().and_then(|v| v.as_object().cloned())
}
fn depth_of(options: Option<&Object<'_>>) -> usize {
let Some(options) = options else {
return 2;
};
match options.get::<_, Value<'_>>("depth") {
Ok(v) if v.is_null() => MAX_DIR_DEPTH,
Ok(v) => v.as_number().map_or(2, |n| n.max(0.0) as usize),
Err(_) => 2,
}
}
fn colors_of(options: Option<&Object<'_>>) -> bool {
options.and_then(|o| o.get::<_, bool>("colors").ok()).unwrap_or(false)
}
fn format_args(args: &[Value<'_>], colors: bool) -> Result<String> {
let mut out = String::new();
Inspector::new(colors).args(&mut out, args)?;
Ok(out)
}
fn inspect<'js>(value: Value<'js>, options: Opt<Value<'js>>) -> Result<String> {
let options = options_of(&options);
let mut out = String::new();
Inspector::new(colors_of(options.as_ref()))
.with_depth(depth_of(options.as_ref()))
.quoted()
.value(&mut out, &value, 0)?;
Ok(out)
}
fn settle_promise<'js>(
resolve: Function<'js>,
reject: Function<'js>,
err: Value<'js>,
rest: Rest<Value<'js>>,
) -> Result<()> {
if err.is_null() || err.is_undefined() {
let value = rest.0.into_iter().next();
match value {
Some(v) => resolve.call::<_, ()>((v,)),
None => resolve.call::<_, ()>(()),
}
} else {
reject.call::<_, ()>((err,))
}
}
fn promisified<'js>(
ctx: Ctx<'js>,
this: This<Value<'js>>,
original: Function<'js>,
args: Rest<Value<'js>>,
) -> Result<Promise<'js>> {
let (promise, resolve, reject) = ctx.promise()?;
let settle = Function::new(ctx.clone(), settle_promise)?;
let callback = bind(&ctx, &settle, vec![resolve.into_value(), reject.into_value()])?;
let mut call = rquickjs::function::Args::new(ctx.clone(), args.0.len() + 1);
call.this(this.0)?;
for arg in args.0 {
call.push_arg(arg)?;
}
call.push_arg(callback)?;
call.apply::<()>(&original)?;
Ok(promise)
}
fn promisify<'js>(ctx: Ctx<'js>, original: Function<'js>) -> Result<Function<'js>> {
if let Ok(custom) = original.get::<_, Function<'js>>("__promisify__") {
return Ok(custom);
}
let body = Function::new(ctx.clone(), promisified)?;
bind(&ctx, &body, vec![original.into_value()])
}
fn settle_callback<'js>(callback: Function<'js>, is_error: bool, value: Value<'js>) -> Result<()> {
if is_error {
callback.call::<_, ()>((value,))
} else {
let ctx = callback.ctx().clone();
callback.call::<_, ()>((Value::new_null(ctx), value))
}
}
fn callbackified<'js>(ctx: Ctx<'js>, this: This<Value<'js>>, original: Function<'js>, args: Rest<Value<'js>>) -> Result<()> {
let mut args = args.0;
let callback: Function<'js> = match args.pop().and_then(|v| v.as_function().cloned()) {
Some(f) => f,
None => {
return Err(rquickjs::Exception::throw_type(
&ctx,
"The last argument must be of type function",
));
},
};
let mut call = rquickjs::function::Args::new(ctx.clone(), args.len());
call.this(this.0)?;
for arg in args {
call.push_arg(arg)?;
}
let promise: Promise<'js> = call.apply(&original)?;
let settle = Function::new(ctx.clone(), settle_callback)?;
let on_ok = bind(&ctx, &settle, vec![callback.clone().into_value(), Value::new_bool(ctx.clone(), false)])?;
let on_err = bind(&ctx, &settle, vec![callback.into_value(), Value::new_bool(ctx.clone(), true)])?;
let then: Function<'js> = promise.get("then")?;
then.call::<_, ()>((This(promise), on_ok, on_err))
}
fn callbackify<'js>(ctx: Ctx<'js>, original: Function<'js>) -> Result<Function<'js>> {
let body = Function::new(ctx.clone(), callbackified)?;
bind(&ctx, &body, vec![original.into_value()])
}
fn deprecated<'js>(
ctx: Ctx<'js>,
this: This<Value<'js>>,
original: Function<'js>,
message: String,
args: Rest<Value<'js>>,
) -> Result<Value<'js>> {
if original.get::<_, bool>("__deprecation_warned__").unwrap_or(false) {
} else {
original.set("__deprecation_warned__", true)?;
if let Ok(console) = ctx.globals().get::<_, Object<'js>>("console") {
if let Ok(warn) = console.get::<_, Function<'js>>("warn") {
warn.call::<_, ()>((format!("DeprecationWarning: {message}"),))?;
}
}
}
let mut call = rquickjs::function::Args::new(ctx.clone(), args.0.len());
call.this(this.0)?;
for arg in args.0 {
call.push_arg(arg)?;
}
call.apply(&original)
}
fn deprecate<'js>(ctx: Ctx<'js>, original: Function<'js>, message: String) -> Result<Function<'js>> {
let body = Function::new(ctx.clone(), deprecated)?;
bind(
&ctx,
&body,
vec![original.into_value(), rquickjs::String::from_str(ctx.clone(), &message)?.into_value()],
)
}
fn is_deep_strict_equal<'js>(a: Value<'js>, b: Value<'js>) -> Result<bool> {
deep_equal(&a, &b, Mode::Strict)
}
fn inherits<'js>(ctor: Function<'js>, super_ctor: Function<'js>) -> Result<()> {
let super_proto: Object<'js> = super_ctor.get("prototype")?;
let proto: Object<'js> = ctor.get("prototype")?;
proto.set_prototype(Some(&super_proto))?;
ctor.set("super_", super_ctor)?;
Ok(())
}
fn tag_of(value: &Value<'_>) -> Result<String> {
let object: Object<'_> = value.ctx().globals().get("Object")?;
let proto: Object<'_> = object.get("prototype")?;
let to_string: Function<'_> = proto.get("toString")?;
to_string.call((This(value.clone()),))
}
const TYPED_ARRAY_TAGS: &[&str] = &[
"[object Int8Array]",
"[object Uint8Array]",
"[object Uint8ClampedArray]",
"[object Int16Array]",
"[object Uint16Array]",
"[object Int32Array]",
"[object Uint32Array]",
"[object Float16Array]",
"[object Float32Array]",
"[object Float64Array]",
"[object BigInt64Array]",
"[object BigUint64Array]",
];
fn types_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
let types = Object::new(ctx.clone())?;
let tagged = |tag: &'static str| {
Func::from(move |value: Value<'_>| -> Result<bool> { Ok(tag_of(&value)? == tag) })
};
types.set("isDate", tagged("[object Date]"))?;
types.set("isRegExp", tagged("[object RegExp]"))?;
types.set("isMap", tagged("[object Map]"))?;
types.set("isSet", tagged("[object Set]"))?;
types.set("isWeakMap", tagged("[object WeakMap]"))?;
types.set("isWeakSet", tagged("[object WeakSet]"))?;
types.set("isPromise", tagged("[object Promise]"))?;
types.set("isArrayBuffer", tagged("[object ArrayBuffer]"))?;
types.set("isSharedArrayBuffer", tagged("[object SharedArrayBuffer]"))?;
types.set("isDataView", tagged("[object DataView]"))?;
types.set("isNativeError", tagged("[object Error]"))?;
types.set("isAsyncFunction", tagged("[object AsyncFunction]"))?;
types.set("isGeneratorFunction", tagged("[object GeneratorFunction]"))?;
types.set("isGeneratorObject", tagged("[object Generator]"))?;
types.set("isArgumentsObject", tagged("[object Arguments]"))?;
types.set(
"isTypedArray",
Func::from(|value: Value<'_>| -> Result<bool> { Ok(TYPED_ARRAY_TAGS.contains(&tag_of(&value)?.as_str())) }),
)?;
types.set(
"isBoxedPrimitive",
Func::from(|value: Value<'_>| -> Result<bool> {
if !value.is_object() {
return Ok(false);
}
let tag = tag_of(&value)?;
Ok(matches!(
tag.as_str(),
"[object String]" | "[object Number]" | "[object Boolean]" | "[object Symbol]" | "[object BigInt]"
))
}),
)?;
Ok(types)
}
pub fn util_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
let util = Object::new(ctx.clone())?;
util.set(
"format",
Func::from(|args: Rest<Value<'_>>| -> Result<String> { format_args(&args.0, false) }),
)?;
util.set(
"formatWithOptions",
Func::from(|options: Opt<Value<'_>>, args: Rest<Value<'_>>| -> Result<String> {
format_args(&args.0, colors_of(options_of(&options).as_ref()))
}),
)?;
let inspect_fn = Function::new(ctx.clone(), inspect)?.with_name("inspect")?;
let symbol: Object<'js> = ctx.globals().get("Symbol")?;
let symbol_for: Function<'js> = symbol.get("for")?;
let custom: Value<'js> = symbol_for.call(("nodejs.util.inspect.custom",))?;
inspect_fn.set("custom", custom)?;
util.set("inspect", inspect_fn)?;
util.set("promisify", Func::from(promisify))?;
util.set("callbackify", Func::from(callbackify))?;
util.set("deprecate", Func::from(deprecate))?;
util.set("inherits", Func::from(inherits))?;
util.set("types", types_object(ctx)?)?;
util.set(
"isDeepStrictEqual",
Func::from(is_deep_strict_equal),
)?;
for name in ["TextEncoder", "TextDecoder"] {
if let Ok(class) = ctx.globals().get::<_, Value<'js>>(name) {
if !class.is_undefined() {
util.set(name, class)?;
}
}
}
Ok(util)
}
pub const UTIL_MEMBERS: &[&str] = &[
"TextDecoder",
"TextEncoder",
"callbackify",
"deprecate",
"format",
"formatWithOptions",
"inherits",
"inspect",
"isDeepStrictEqual",
"promisify",
"types",
];