Skip to content

Commit c6c34dc

Browse files
Add support for Proxy objects in JSON serialization and logging (#1401)
1 parent 3dfbed3 commit c6c34dc

File tree

5 files changed

+107
-7
lines changed

5 files changed

+107
-7
lines changed

libs/llrt_json/src/stringify.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ fn run_to_json<'js>(
133133

134134
//only preserve indentation if we're returning nested data
135135
let indentation = context.indentation.and_then(|indentation| {
136-
matches!(val.type_of(), Type::Object | Type::Array | Type::Exception).then_some(indentation)
136+
matches!(
137+
val.type_of(),
138+
Type::Object | Type::Array | Type::Exception | Type::Proxy
139+
)
140+
.then_some(indentation)
137141
});
138142

139143
append_value(
@@ -438,7 +442,7 @@ fn iterate<'js>(
438442
let ctx = context.ctx;
439443
let indentation = context.indentation;
440444
match elem.type_of() {
441-
Type::Object | Type::Exception => {
445+
Type::Object | Type::Exception | Type::Proxy => {
442446
let js_object = unsafe { elem.as_object().unwrap_unchecked() };
443447
if js_object.contains_key(PredefinedAtom::ToJSON)? {
444448
return run_to_json(context, js_object);
@@ -461,8 +465,25 @@ fn iterate<'js>(
461465

462466
value_written = false;
463467

464-
for key in js_object.keys::<String>() {
465-
let key = key?;
468+
// Collect keys: js_object.keys() uses JS_GetOwnPropertyNames which can fail for
469+
// Proxy objects. Fall back to Object.keys() in that case.
470+
let keys: Vec<String> = {
471+
let collected: Vec<String> = js_object.keys::<String>().flatten().collect();
472+
if collected.is_empty() {
473+
// Clear any pending exception and try Object.keys() for Proxy support
474+
ctx.catch();
475+
ctx.globals()
476+
.get::<_, Object>("Object")
477+
.ok()
478+
.and_then(|o| o.get::<_, Function>("keys").ok())
479+
.and_then(|f| f.call::<_, Vec<String>>((js_object.clone(),)).ok())
480+
.unwrap_or_default()
481+
} else {
482+
collected
483+
}
484+
};
485+
486+
for key in keys {
466487
let val = js_object.get(&key)?;
467488

468489
add_comma = append_value(

libs/llrt_logging/src/lib.rs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ fn format_raw_inner<'js>(
455455
result.push('}');
456456
return Ok(());
457457
},
458-
Type::Array | Type::Object | Type::Exception => {
458+
Type::Array | Type::Object | Type::Exception | Type::Proxy => {
459459
let hash = hash::default_hash(&value);
460460
if visited.contains(&hash) {
461461
if color_enabled {
@@ -661,10 +661,71 @@ fn write_object<'js>(
661661
let mut keys = obj.keys();
662662
let mut filter_functions = false;
663663
if !is_array && keys.len() == 0 {
664+
// Clear any pending exception from JS_GetOwnPropertyNames (e.g. on Proxy objects)
665+
let ctx = obj.ctx();
666+
ctx.catch();
667+
// Try Object.keys() which correctly handles Proxy objects
668+
let proxy_keys: Option<Vec<String>> = ctx
669+
.globals()
670+
.get::<_, Object>("Object")
671+
.ok()
672+
.and_then(|o| o.get::<_, Function>("keys").ok())
673+
.and_then(|f| f.call::<_, Vec<String>>((obj.clone(),)).ok());
674+
if let Some(pk) = proxy_keys {
675+
if !pk.is_empty() {
676+
let apply_indentation = depth < 2;
677+
let mut first = false;
678+
let length = pk.len();
679+
for (i, key) in pk.iter().enumerate() {
680+
let value: Value = obj.get::<&str, _>(key.as_str())?;
681+
let numeric_key = key.parse::<f64>().is_ok();
682+
write_sep(result, first, apply_indentation, options.newline);
683+
if apply_indentation {
684+
push_indentation(result, depth + 1);
685+
}
686+
if depth > MAX_INDENTATION_LEVEL - 1 {
687+
result.push(SPACING);
688+
}
689+
format_raw_string_inner(
690+
result,
691+
key.clone(),
692+
numeric_key,
693+
numeric_key & color_enabled,
694+
);
695+
if numeric_key && color_enabled {
696+
Color::reset(result);
697+
}
698+
result.push(':');
699+
result.push(SPACING);
700+
format_raw_inner(result, value, options, visited, depth + 1)?;
701+
first = true;
702+
if i > 99 {
703+
result.push_str("... ");
704+
let mut buffer = itoa::Buffer::new();
705+
result.push_str(buffer.format(length - i));
706+
result.push_str(" more items");
707+
break;
708+
}
709+
}
710+
if first {
711+
if apply_indentation {
712+
result.push(if options.newline {
713+
NEWLINE
714+
} else {
715+
CARRIAGE_RETURN
716+
});
717+
push_indentation(result, depth);
718+
} else {
719+
result.push(SPACING);
720+
}
721+
}
722+
result.push('}');
723+
return Ok(());
724+
}
725+
}
664726
if let Some(proto) = obj.get_prototype() {
665727
if proto != options.object_prototype {
666728
keys = proto.own_keys(options.object_filter);
667-
668729
filter_functions = true;
669730
}
670731
}

libs/llrt_utils/src/clone.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::collections::HashSet;
55
use rquickjs::{
66
atom::PredefinedAtom,
77
function::{Constructor, Opt, This},
8-
Array, ArrayBuffer, Ctx, Function, IntoJs, Null, Object, Result, Type, Value,
8+
Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, Null, Object, Result, Type, Value,
99
};
1010

1111
use super::{
@@ -79,6 +79,12 @@ pub fn structured_clone<'js>(
7979
}
8080
}
8181
match value.type_of() {
82+
Type::Proxy => {
83+
return Err(Exception::throw_type(
84+
ctx,
85+
"A Proxy value could not be cloned",
86+
));
87+
},
8288
Type::Object => {
8389
if check_circular(
8490
&mut tape,

modules/llrt_assert/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn ok(ctx: Ctx, value: Value, message: Opt<Value>) -> Result<()> {
2929
| Type::Constructor
3030
| Type::Exception
3131
| Type::Function
32+
| Type::Proxy
3233
| Type::Symbol
3334
| Type::Object => {
3435
return Ok(());

tests/unit/console.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ it("should log complex object", () => {
166166
);
167167
});
168168

169+
it("should log Proxy object", () => {
170+
const target = { a: 1, b: "foo" };
171+
const proxy = new Proxy(target, {
172+
set(t, p, v) {
173+
t[p] = v;
174+
return true;
175+
},
176+
});
177+
expect(util.format(proxy)).toEqual(`{\n a: 1,\n b: 'foo'\n}`);
178+
});
179+
169180
it("should log Headers", () => {
170181
const headers = new Headers();
171182
headers.append("foo", "bar");

0 commit comments

Comments
 (0)