Skip to main content

ferrijs_std/node/
util.rs

1//! `node:util`.
2//!
3//! Rendering (`format`, `formatWithOptions`, `inspect`) runs through the
4//! one [`Inspector`](super::inspect::Inspector) the `console` global uses,
5//! so a value prints the same wherever it is printed.
6//!
7//! The wrappers (`promisify`, `callbackify`, `deprecate`) hold their target
8//! through `Function.prototype.bind` rather than a Rust closure: a native
9//! closure that captured a JS value would form a GC cycle the runtime
10//! cannot trace, which aborts at teardown.
11
12use rquickjs::function::{Func, Opt, Rest, This};
13use rquickjs::{Ctx, Function, Object, Promise, Result, Value};
14
15use super::deep_equal::{Mode, deep_equal};
16use super::inspect::{Inspector, MAX_DIR_DEPTH};
17
18/// `Function.prototype.bind`, with `this` left undefined.
19fn bind<'js>(ctx: &Ctx<'js>, target: &Function<'js>, args: Vec<Value<'js>>) -> Result<Function<'js>> {
20  let bind_fn: Function<'js> = target.get("bind")?;
21  let mut call_args = rquickjs::function::Args::new(ctx.clone(), args.len() + 1);
22  call_args.this(target.clone())?;
23  call_args.push_arg(Value::new_undefined(ctx.clone()))?;
24  for arg in args {
25    call_args.push_arg(arg)?;
26  }
27  call_args.apply(&bind_fn)
28}
29
30fn options_of<'js>(options: &Opt<Value<'js>>) -> Option<Object<'js>> {
31  options.0.as_ref().and_then(|v| v.as_object().cloned())
32}
33
34/// `depth` from an options bag, with Node's `null` meaning "as deep as it
35/// goes" (bounded, so a huge graph cannot wedge the renderer).
36fn depth_of(options: Option<&Object<'_>>) -> usize {
37  let Some(options) = options else {
38    return 2;
39  };
40  match options.get::<_, Value<'_>>("depth") {
41    Ok(v) if v.is_null() => MAX_DIR_DEPTH,
42    Ok(v) => v.as_number().map_or(2, |n| n.max(0.0) as usize),
43    Err(_) => 2,
44  }
45}
46
47fn colors_of(options: Option<&Object<'_>>) -> bool {
48  options.and_then(|o| o.get::<_, bool>("colors").ok()).unwrap_or(false)
49}
50
51fn format_args(args: &[Value<'_>], colors: bool) -> Result<String> {
52  let mut out = String::new();
53  Inspector::new(colors).args(&mut out, args)?;
54  Ok(out)
55}
56
57fn inspect<'js>(value: Value<'js>, options: Opt<Value<'js>>) -> Result<String> {
58  let options = options_of(&options);
59  let mut out = String::new();
60  Inspector::new(colors_of(options.as_ref()))
61    .with_depth(depth_of(options.as_ref()))
62    .quoted()
63    .value(&mut out, &value, 0)?;
64  Ok(out)
65}
66
67/// The callback half of a promisified call: `(resolve, reject, err, value)`,
68/// with the first two bound in.
69fn settle_promise<'js>(
70  resolve: Function<'js>,
71  reject: Function<'js>,
72  err: Value<'js>,
73  rest: Rest<Value<'js>>,
74) -> Result<()> {
75  if err.is_null() || err.is_undefined() {
76    let value = rest.0.into_iter().next();
77    match value {
78      Some(v) => resolve.call::<_, ()>((v,)),
79      None => resolve.call::<_, ()>(()),
80    }
81  } else {
82    reject.call::<_, ()>((err,))
83  }
84}
85
86/// The body of a promisified function: `(original, ...args)` with the
87/// original bound in.
88fn promisified<'js>(
89  ctx: Ctx<'js>,
90  this: This<Value<'js>>,
91  original: Function<'js>,
92  args: Rest<Value<'js>>,
93) -> Result<Promise<'js>> {
94  let (promise, resolve, reject) = ctx.promise()?;
95  let settle = Function::new(ctx.clone(), settle_promise)?;
96  let callback = bind(&ctx, &settle, vec![resolve.into_value(), reject.into_value()])?;
97
98  let mut call = rquickjs::function::Args::new(ctx.clone(), args.0.len() + 1);
99  call.this(this.0)?;
100  for arg in args.0 {
101    call.push_arg(arg)?;
102  }
103  call.push_arg(callback)?;
104  call.apply::<()>(&original)?;
105  Ok(promise)
106}
107
108fn promisify<'js>(ctx: Ctx<'js>, original: Function<'js>) -> Result<Function<'js>> {
109  // Node honours a `util.promisify.custom` implementation on the target.
110  if let Ok(custom) = original.get::<_, Function<'js>>("__promisify__") {
111    return Ok(custom);
112  }
113  let body = Function::new(ctx.clone(), promisified)?;
114  bind(&ctx, &body, vec![original.into_value()])
115}
116
117/// The settle half of a callbackified call: `(callback, is_error, value)`,
118/// with the first two bound in.
119fn settle_callback<'js>(callback: Function<'js>, is_error: bool, value: Value<'js>) -> Result<()> {
120  if is_error {
121    callback.call::<_, ()>((value,))
122  } else {
123    let ctx = callback.ctx().clone();
124    callback.call::<_, ()>((Value::new_null(ctx), value))
125  }
126}
127
128/// The body of a callbackified function: `(original, ...args, callback)`
129/// with the original bound in.
130fn callbackified<'js>(ctx: Ctx<'js>, this: This<Value<'js>>, original: Function<'js>, args: Rest<Value<'js>>) -> Result<()> {
131  let mut args = args.0;
132  let callback: Function<'js> = match args.pop().and_then(|v| v.as_function().cloned()) {
133    Some(f) => f,
134    None => {
135      return Err(rquickjs::Exception::throw_type(
136        &ctx,
137        "The last argument must be of type function",
138      ));
139    },
140  };
141
142  let mut call = rquickjs::function::Args::new(ctx.clone(), args.len());
143  call.this(this.0)?;
144  for arg in args {
145    call.push_arg(arg)?;
146  }
147  let promise: Promise<'js> = call.apply(&original)?;
148
149  let settle = Function::new(ctx.clone(), settle_callback)?;
150  let on_ok = bind(&ctx, &settle, vec![callback.clone().into_value(), Value::new_bool(ctx.clone(), false)])?;
151  let on_err = bind(&ctx, &settle, vec![callback.into_value(), Value::new_bool(ctx.clone(), true)])?;
152  let then: Function<'js> = promise.get("then")?;
153  then.call::<_, ()>((This(promise), on_ok, on_err))
154}
155
156fn callbackify<'js>(ctx: Ctx<'js>, original: Function<'js>) -> Result<Function<'js>> {
157  let body = Function::new(ctx.clone(), callbackified)?;
158  bind(&ctx, &body, vec![original.into_value()])
159}
160
161/// The body of a deprecated function: `(original, message, ...args)` with
162/// the first two bound in. The warning fires once per wrapped function, as
163/// Node's does.
164fn deprecated<'js>(
165  ctx: Ctx<'js>,
166  this: This<Value<'js>>,
167  original: Function<'js>,
168  message: String,
169  args: Rest<Value<'js>>,
170) -> Result<Value<'js>> {
171  if original.get::<_, bool>("__deprecation_warned__").unwrap_or(false) {
172    // already warned
173  } else {
174    original.set("__deprecation_warned__", true)?;
175    if let Ok(console) = ctx.globals().get::<_, Object<'js>>("console") {
176      if let Ok(warn) = console.get::<_, Function<'js>>("warn") {
177        warn.call::<_, ()>((format!("DeprecationWarning: {message}"),))?;
178      }
179    }
180  }
181  let mut call = rquickjs::function::Args::new(ctx.clone(), args.0.len());
182  call.this(this.0)?;
183  for arg in args.0 {
184    call.push_arg(arg)?;
185  }
186  call.apply(&original)
187}
188
189fn deprecate<'js>(ctx: Ctx<'js>, original: Function<'js>, message: String) -> Result<Function<'js>> {
190  let body = Function::new(ctx.clone(), deprecated)?;
191  bind(
192    &ctx,
193    &body,
194    vec![original.into_value(), rquickjs::String::from_str(ctx.clone(), &message)?.into_value()],
195  )
196}
197
198fn is_deep_strict_equal<'js>(a: Value<'js>, b: Value<'js>) -> Result<bool> {
199  deep_equal(&a, &b, Mode::Strict)
200}
201
202/// `util.inherits`: point one constructor's prototype chain at another's.
203fn inherits<'js>(ctor: Function<'js>, super_ctor: Function<'js>) -> Result<()> {
204  let super_proto: Object<'js> = super_ctor.get("prototype")?;
205  let proto: Object<'js> = ctor.get("prototype")?;
206  proto.set_prototype(Some(&super_proto))?;
207  ctor.set("super_", super_ctor)?;
208  Ok(())
209}
210
211/// `Object.prototype.toString.call(value)`, the tag every `util.types`
212/// predicate is defined in terms of.
213fn tag_of(value: &Value<'_>) -> Result<String> {
214  let object: Object<'_> = value.ctx().globals().get("Object")?;
215  let proto: Object<'_> = object.get("prototype")?;
216  let to_string: Function<'_> = proto.get("toString")?;
217  to_string.call((This(value.clone()),))
218}
219
220const TYPED_ARRAY_TAGS: &[&str] = &[
221  "[object Int8Array]",
222  "[object Uint8Array]",
223  "[object Uint8ClampedArray]",
224  "[object Int16Array]",
225  "[object Uint16Array]",
226  "[object Int32Array]",
227  "[object Uint32Array]",
228  "[object Float16Array]",
229  "[object Float32Array]",
230  "[object Float64Array]",
231  "[object BigInt64Array]",
232  "[object BigUint64Array]",
233];
234
235fn types_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
236  let types = Object::new(ctx.clone())?;
237  let tagged = |tag: &'static str| {
238    Func::from(move |value: Value<'_>| -> Result<bool> { Ok(tag_of(&value)? == tag) })
239  };
240  types.set("isDate", tagged("[object Date]"))?;
241  types.set("isRegExp", tagged("[object RegExp]"))?;
242  types.set("isMap", tagged("[object Map]"))?;
243  types.set("isSet", tagged("[object Set]"))?;
244  types.set("isWeakMap", tagged("[object WeakMap]"))?;
245  types.set("isWeakSet", tagged("[object WeakSet]"))?;
246  types.set("isPromise", tagged("[object Promise]"))?;
247  types.set("isArrayBuffer", tagged("[object ArrayBuffer]"))?;
248  types.set("isSharedArrayBuffer", tagged("[object SharedArrayBuffer]"))?;
249  types.set("isDataView", tagged("[object DataView]"))?;
250  types.set("isNativeError", tagged("[object Error]"))?;
251  types.set("isAsyncFunction", tagged("[object AsyncFunction]"))?;
252  types.set("isGeneratorFunction", tagged("[object GeneratorFunction]"))?;
253  types.set("isGeneratorObject", tagged("[object Generator]"))?;
254  types.set("isArgumentsObject", tagged("[object Arguments]"))?;
255  types.set(
256    "isTypedArray",
257    Func::from(|value: Value<'_>| -> Result<bool> { Ok(TYPED_ARRAY_TAGS.contains(&tag_of(&value)?.as_str())) }),
258  )?;
259  types.set(
260    "isBoxedPrimitive",
261    Func::from(|value: Value<'_>| -> Result<bool> {
262      if !value.is_object() {
263        return Ok(false);
264      }
265      let tag = tag_of(&value)?;
266      Ok(matches!(
267        tag.as_str(),
268        "[object String]" | "[object Number]" | "[object Boolean]" | "[object Symbol]" | "[object BigInt]"
269      ))
270    }),
271  )?;
272  Ok(types)
273}
274
275/// Every `util` export on one object, for both the ES module and the
276/// `require` namespace.
277///
278/// # Errors
279///
280/// Propagates the property writes and the global reads it makes.
281pub fn util_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
282  let util = Object::new(ctx.clone())?;
283
284  util.set(
285    "format",
286    Func::from(|args: Rest<Value<'_>>| -> Result<String> { format_args(&args.0, false) }),
287  )?;
288  util.set(
289    "formatWithOptions",
290    Func::from(|options: Opt<Value<'_>>, args: Rest<Value<'_>>| -> Result<String> {
291      format_args(&args.0, colors_of(options_of(&options).as_ref()))
292    }),
293  )?;
294
295  let inspect_fn = Function::new(ctx.clone(), inspect)?.with_name("inspect")?;
296  // `util.inspect.custom` — the symbol a class implements to control how it
297  // renders. Exposed so third-party code can read it; the renderer does not
298  // call it yet.
299  let symbol: Object<'js> = ctx.globals().get("Symbol")?;
300  let symbol_for: Function<'js> = symbol.get("for")?;
301  let custom: Value<'js> = symbol_for.call(("nodejs.util.inspect.custom",))?;
302  inspect_fn.set("custom", custom)?;
303  util.set("inspect", inspect_fn)?;
304
305  util.set("promisify", Func::from(promisify))?;
306  util.set("callbackify", Func::from(callbackify))?;
307  util.set("deprecate", Func::from(deprecate))?;
308  util.set("inherits", Func::from(inherits))?;
309  util.set("types", types_object(ctx)?)?;
310  util.set(
311    "isDeepStrictEqual",
312    Func::from(is_deep_strict_equal),
313  )?;
314
315  // The text codecs are web-platform globals this runtime already installs;
316  // `util` re-exports the same objects rather than defining its own.
317  for name in ["TextEncoder", "TextDecoder"] {
318    if let Ok(class) = ctx.globals().get::<_, Value<'js>>(name) {
319      if !class.is_undefined() {
320        util.set(name, class)?;
321      }
322    }
323  }
324
325  Ok(util)
326}
327
328/// The names [`util_object`] sets, for a module's export list.
329pub const UTIL_MEMBERS: &[&str] = &[
330  "TextDecoder",
331  "TextEncoder",
332  "callbackify",
333  "deprecate",
334  "format",
335  "formatWithOptions",
336  "inherits",
337  "inspect",
338  "isDeepStrictEqual",
339  "promisify",
340  "types",
341];