Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,7 @@
NativeCode,

JSX,
DOMNode,
Event,

GetterSetter,
Expand Down Expand Up @@ -2010,6 +2011,7 @@
| Tag::Error
| Tag::Class
| Tag::Event
| Tag::DOMNode
)
}
}
Expand Down Expand Up @@ -2044,6 +2046,7 @@
ToJSON,
NativeCode,
JSX,
DOMNode,
Event,
GetterSetter,
CustomGetterSetter,
Expand Down Expand Up @@ -2090,6 +2093,7 @@
TagPayload::ToJSON => Tag::ToJSON,
TagPayload::NativeCode => Tag::NativeCode,
TagPayload::JSX => Tag::JSX,
TagPayload::DOMNode => Tag::DOMNode,
TagPayload::Event => Tag::Event,
TagPayload::GetterSetter => Tag::GetterSetter,
TagPayload::CustomGetterSetter => Tag::CustomGetterSetter,
Expand Down Expand Up @@ -2135,6 +2139,7 @@
Tag::ToJSON => TagPayload::ToJSON,
Tag::NativeCode => TagPayload::NativeCode,
Tag::JSX => TagPayload::JSX,
Tag::DOMNode => TagPayload::DOMNode,
Tag::Event => TagPayload::Event,
Tag::GetterSetter => TagPayload::GetterSetter,
Tag::CustomGetterSetter => TagPayload::CustomGetterSetter,
Expand Down Expand Up @@ -2330,6 +2335,16 @@
}
}

// Is this a DOM node (jsdom / happy-dom)?
if matches!(js_type, jsc::JSType::Object | jsc::JSType::FinalObject)
&& is_dom_node(global_this, value)?
{
return Ok(TagResult {
tag: TagPayload::DOMNode,
cell: js_type,
});
}

use jsc::JSType as T;
let tag = match js_type {
T::ErrorInstance => TagPayload::Error,
Expand Down Expand Up @@ -3304,6 +3319,67 @@
Ok(None)
}

const DOM_ELEMENT_NODE: i32 = 1;
const DOM_TEXT_NODE: i32 = 3;
const DOM_COMMENT_NODE: i32 = 8;
const DOM_FRAGMENT_NODE: i32 = 11;

/// Mirrors the pretty-format `DOMElement` plugin test: given a constructor
/// name, return the `nodeType` value that would confirm the object is a DOM
/// node of that kind, or `None` if the name does not look DOM-like.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn dom_node_type_for_class_name(name: &[u8]) -> Option<i32> {
use bun_core::strings;
// /^((HTML|SVG)\w*)?Element$/
if strings::has_suffix_comptime(name, b"Element")
&& (name.len() == b"Element".len()
|| strings::has_prefix_comptime(name, b"HTML")
|| strings::has_prefix_comptime(name, b"SVG"))
{
return Some(DOM_ELEMENT_NODE);
}
match name {
b"Text" => Some(DOM_TEXT_NODE),
b"Comment" => Some(DOM_COMMENT_NODE),
b"DocumentFragment" => Some(DOM_FRAGMENT_NODE),
_ => None,
}
}

/// DOM node detection (jsdom / happy-dom). Restricted to values whose
/// class name matches a known DOM pattern so the `nodeType` prototype
/// getter is never invoked on unrelated objects.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline(never)]
pub fn is_dom_node(global_this: &JSGlobalObject, value: JSValue) -> JsResult<bool> {
// Fast bail for plain `{}` / `Object.create(null)` / one-level classes:
// DOM nodes inherit through at least `Node` → `EventTarget`, so an
// object whose prototype is `Object.prototype` (grand-proto is null)
// cannot be one and skips the `calculatedClassName` walk below.
Comment thread
robobun marked this conversation as resolved.
Outdated
let proto = value.get_prototype(global_this);
if proto.is_empty_or_undefined_or_null() {
return Ok(false);
}
let grand = proto.get_prototype(global_this);
if grand.is_empty_or_undefined_or_null() {
return Ok(false);
}

Check failure on line 3364 in src/jsc/ConsoleObject.rs

View check run for this annotation

Claude / Claude Code Review

get_prototype on proto can invoke Proxy trap and leave a pending exception

The fast-bail probe calls `proto.get_prototype(global_this)` at line 3361, but `proto` can be a Proxy — e.g. `Bun.inspect(Object.create(new Proxy({}, { getPrototypeOf() { throw 0 } })))` — so this invokes the user's `getPrototypeOf` trap, and on throw returns empty with the exception left pending while `is_dom_node` returns `Ok(false)`. Add an exception check/clear after the second `get_prototype` (same as you already do for the `nodeType` getter at line 3376), or use a direct-prototype accessor
Comment thread
claude[bot] marked this conversation as resolved.

let mut name_str = ZigString::init(b"");
value.get_class_name(global_this, &mut name_str)?;
let name_slice = name_str.to_slice();
let Some(expected) = dom_node_type_for_class_name(name_slice.slice()) else {
return Ok(false);
};
let confirmed = match value.get(global_this, "nodeType") {
Ok(Some(n)) if n.is_int32() => n.to_int32() == expected,
Ok(_) => false,
Err(_) => {
global_this.clear_exception_except_termination();
false
}
};
Ok(confirmed)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// `JSGlobalObject` is an opaque `UnsafeCell`-backed ZST handle; remaining
// params are by-value `JSValue`/scalars → `safe fn`.
unsafe extern "C" {
Expand Down Expand Up @@ -3470,6 +3546,7 @@
&mut remove_before_recurse,
),
Tag::JSX => self.print_jsx::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::DOMNode => self.print_dom_node::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::Object => self.print_object::<ENABLE_ANSI_COLORS>(writer_, value, js_type),
Tag::TypedArray => {
self.print_typed_array::<ENABLE_ANSI_COLORS>(writer_, value, js_type)
Expand Down Expand Up @@ -5449,6 +5526,215 @@
Ok(())
}

/// Serializes a DOM node (jsdom / happy-dom) as markup, mirroring the
/// `pretty-format` `DOMElement` plugin so jest-dom matcher messages and
/// snapshots render `<button id="x">text</button>` instead of the full
/// object graph.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline(never)]
fn print_dom_node<const C: bool>(
&mut self,
writer_: &mut dyn bun_io::Write,
value: JSValue,
) -> JsResult<()> {
macro_rules! pf {
($s:literal) => {
pfmt!($s, C)
};
}
macro_rules! get_swallow {
($v:expr, $name:literal) => {
match $v.get(self.global_this, $name) {
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception_except_termination();
None
}
}
};
}

let node_type = get_swallow!(value, "nodeType")
.filter(|v| v.is_int32())
.map(|v| v.to_int32())
.unwrap_or(0);

if matches!(node_type, DOM_TEXT_NODE | DOM_COMMENT_NODE) {
let data = get_swallow!(value, "data");
let text = match data {
Some(v) if v.is_string() => {
bun_core::OwnedString::new(v.to_bun_string(self.global_this)?)
}
_ => bun_core::OwnedString::new(bun_core::String::empty()),
};
if node_type == DOM_COMMENT_NODE {
let _ = write!(writer_, "{}<!--{}-->{}", pf!("<r><d>"), text, pf!("<r>"));
} else {
let _ = write!(writer_, "{}", text);
}
return Ok(());
}
Comment thread
robobun marked this conversation as resolved.

let (tag_utf8, is_fragment) = if node_type == DOM_FRAGMENT_NODE {
(
bun_core::ZigStringSlice::from_utf8_never_free(b"DocumentFragment"),
true,
)
} else {
let tag_name = get_swallow!(value, "tagName");
match tag_name {
Some(v) if v.is_string() => {
let s = v.get_zig_string(self.global_this)?;
let slice = s.to_slice();
let mut owned = slice.slice().to_vec();
owned.make_ascii_lowercase();
(bun_core::ZigStringSlice::init_owned(owned), false)
}
_ => (
bun_core::ZigStringSlice::from_utf8_never_free(b"unknown"),
false,
),
}
};
let tag_bytes = tag_utf8.slice();

if self.depth >= self.max_depth {
let _ = write!(
writer_,
"{}<{}{} \u{2026} {}/>{}",
pf!("<r><green>"),
bstr::BStr::new(tag_bytes),
pf!("<r>"),
pf!("<green>"),
pf!("<r>"),
);
return Ok(());
}

let _ = writer_.write_all(pf!("<r><green>").as_bytes());
let _ = writer_.write_all(b"<");
let _ = writer_.write_all(tag_bytes);
let _ = writer_.write_all(pf!("<r>").as_bytes());

let mut attrs_multiline = false;
if !is_fragment {
if let Some(attrs) = get_swallow!(value, "attributes") {
if attrs.is_cell() && attrs.is_object() {
if let Some(len_v) = get_swallow!(attrs, "length") {
if len_v.is_int32() {
let n = len_v.to_int32().max(0) as u32;
let mut pairs: Vec<(Vec<u8>, bun_core::OwnedString)> =
Vec::with_capacity(n.min(64) as usize);
for i in 0..n {
Comment thread
robobun marked this conversation as resolved.
let Ok(attr) = attrs.get_index(self.global_this, i) else {
self.global_this.clear_exception_except_termination();
continue;
};
if !attr.is_cell() || !attr.is_object() {
continue;
}
let Some(name) = get_swallow!(attr, "name") else {
continue;
};
if !name.is_string() {
continue;
}
let name_s = name.get_zig_string(self.global_this)?;
let name_owned = name_s.to_slice().slice().to_vec();
let val = get_swallow!(attr, "value")
.filter(|v| v.is_string())
.map(|v| v.to_bun_string(self.global_this))
.transpose()?
.map(bun_core::OwnedString::new)
.unwrap_or_else(|| {
bun_core::OwnedString::new(bun_core::String::empty())
});
pairs.push((name_owned, val));
}
pairs.sort_by(|a, b| a.0.cmp(&b.0));

Check warning on line 5653 in src/jsc/ConsoleObject.rs

View check run for this annotation

Claude / Claude Code Review

Redundant !pairs.is_empty() in attrs_multiline condition

Nit: `!pairs.is_empty()` is redundant here — `pairs.len() > 1` already implies non-empty. Can simplify to `!self.single_line && pairs.len() > 1`.
Comment thread
claude[bot] marked this conversation as resolved.
attrs_multiline =
!self.single_line && !pairs.is_empty() && pairs.len() > 1;
for (name, val) in &pairs {
if attrs_multiline {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent + 1, writer_);
} else {
let _ = writer_.write_all(b" ");
}
let _ = write!(
writer_,
"{}{}{}={}\"{}\"{}",
pf!("<blue>"),
bstr::BStr::new(name),
pf!("<r><d>"),
pf!("<r><green>"),
val,
pf!("<r>"),
);
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
}
if attrs_multiline {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
}
}
}
}
}

let children =
get_swallow!(value, "childNodes").filter(|v| v.is_cell() && v.is_object());
let child_len = children
.and_then(|c| get_swallow!(c, "length"))
.filter(|v| v.is_int32())
.map(|v| v.to_int32().max(0) as u32)
.unwrap_or(0);

if child_len == 0 {
if attrs_multiline {
let _ = write!(writer_, "{}/>{}", pf!("<green>"), pf!("<r>"));
} else {
let _ = write!(writer_, "{} />{}", pf!("<green>"), pf!("<r>"));
}
return Ok(());
}

let _ = write!(writer_, "{}>{}", pf!("<green>"), pf!("<r>"));
{
self.indent += 1;
self.depth += 1;
let _ind = defer_decrement!(self.indent);
let _dep = defer_decrement!(self.depth);
let children = children.expect("child_len > 0 implies Some");
for i in 0..child_len {
if !self.single_line {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
let Ok(child) = children.get_index(self.global_this, i) else {
self.global_this.clear_exception_except_termination();
continue;
};
if !child.is_cell() {
continue;
}
let tag = Tag::get_advanced(child, self.global_this, self.tag_opts())?;
self.format::<C>(tag, writer_, child, self.global_this)?;
}
}
if !self.single_line {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
let _ = write!(
writer_,
"{}</{}>{}",
pf!("<green>"),
bstr::BStr::new(tag_bytes),
pf!("<r>"),
);
Ok(())
}

#[inline(never)]
fn print_object<const C: bool>(
&mut self,
Expand Down
Loading
Loading