From df89f74c931d6048b78d6f80e2bc9ed42cc0dc21 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:00:58 +0000 Subject: [PATCH 1/8] assert: compare proxies by their target in deepStrictEqual The strict deep-equality path rejected any pair where the two objects report different class names. JSC reports a Proxy's own class name, not its target's, so assert.deepStrictEqual(new Proxy({}, {}), {}) failed even though every trap forwards to an identical target. Node compares prototypes here, never class names. Skip the class-name check when either side is a proxy; the prototype check and the own-property walk above and below it already go through the traps. --- src/jsc/bindings/bindings.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 42083443e7b8..bfbcdd521508 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -936,8 +936,14 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, } if constexpr (isStrict && !skipPrototypeIdentity) { - if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) { - return false; + // A Proxy is transparent to this comparison: its traps forward + // [[GetPrototypeOf]] and own-property enumeration to the target, but + // JSC reports the proxy's own class name, which would reject every + // proxy/target pair. node compares prototypes, never class names. + if (!o1->isProxy() && !o2->isProxy()) { + if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) { + return false; + } } } From 473428409b9544847eee53cc9d82f2ba7c4f0614 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:01:08 +0000 Subject: [PATCH 2/8] util: implement util.diff() Adds the v26 util.diff(actual, expected) API, which reports the Myers diff of two strings or two string arrays as [operation, value] pairs (-1 delete, 0 unchanged, 1 insert). The existing native myersDiff comparator only accepted strings; it now also takes arrays of strings, diffing one element per line, so the public API and node:assert share a single implementation. --- src/js/node/util.ts | 61 +++++++++++++++++++++++++ src/runtime/node/node_assert.rs | 36 +++++++++++++++ src/runtime/node/node_assert_binding.rs | 18 +++++++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/js/node/util.ts b/src/js/node/util.ts index a486d759ce62..af370d8a96ae 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -245,6 +245,66 @@ function styleText(format, text) { return `\u001b[${formatCodes[0]}m${text}\u001b[${formatCodes[1]}m`; } +// Port of node's `internal/util/diff` (v26.3.0), the implementation behind +// `util.diff()`. +// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/diff.js +// +// The native comparator reports `{ kind, value }` with kind Insert=0, Delete=1, +// Equal=2; node's public shape is `[operation, value]` with INSERT=1, +// DELETE=-1, NOP=0. +// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/assert/myers_diff.js#L18-L22 +const kOperationForDiffKind = [1, -1, 0]; +let myersDiff; + +function validateDiffInput(value, name) { + if (!$isJSArray(value)) { + validateString(value, name); + return; + } + for (let i = 0; i < value.length; ++i) { + if (typeof value[i] !== "string") { + throw $ERR_INVALID_ARG_TYPE(`${name}[${i}]`, "string", value[i]); + } + } +} + +// node's myersDiff indexes both operands uniformly, so a string paired with an +// array is compared code-unit-to-element. The native comparator takes two +// strings or two arrays, so widen the odd one out. +function toDiffLines(value) { + if ($isJSArray(value)) return value; + const length = value.length; + const lines = $newArrayWithSize(length); + for (let i = 0; i < length; i++) { + lines[i] = value[i]; + } + return lines; +} + +function diff(actual, expected) { + if (actual === expected) { + return []; + } + + validateDiffInput(actual, "actual"); + validateDiffInput(expected, "expected"); + + myersDiff ??= require("internal/assert/myers_diff").myersDiff; + const raw = + $isJSArray(actual) === $isJSArray(expected) + ? myersDiff(actual, expected) + : myersDiff(toDiffLines(actual), toDiffLines(expected)); + + // myersDiff walks the edit path backwards; node reverses it before returning. + const length = raw.length; + const result = $newArrayWithSize(length); + for (let i = 0; i < length; i++) { + const { kind, value } = raw[length - 1 - i]; + result[i] = [kOperationForDiffKind[kind], typeof value === "number" ? String.fromCharCode(value) : value]; + } + return result; +} + function getSystemErrorName(err: any) { if (typeof err !== "number") throw $ERR_INVALID_ARG_TYPE("err", "number", err); if (err >= 0 || !NumberIsSafeInteger(err)) throw $ERR_OUT_OF_RANGE("err", "a negative integer", err); @@ -481,6 +541,7 @@ cjs_exports = { TextEncoder, MIMEType, MIMEParams, + diff, // Deprecated in Node.js 22, removed in 23 isArray: $isArray, diff --git a/src/runtime/node/node_assert.rs b/src/runtime/node/node_assert.rs index d3bdb9b83b6c..5dd255b55ba9 100644 --- a/src/runtime/node/node_assert.rs +++ b/src/runtime/node/node_assert.rs @@ -87,6 +87,42 @@ pub(crate) fn myers_diff( } } +/// `util.diff()` accepts arrays of strings as well as strings. Each element is +/// one line, compared byte-wise after UTF-8 conversion. +pub(crate) fn myers_diff_arrays( + global: &JSGlobalObject, + actual: JSValue, + expected: JSValue, + check_comma_disparity: bool, +) -> JsResult { + let actual_lines = collect_utf8_elements(global, actual)?; + let expected_lines = collect_utf8_elements(global, expected)?; + if actual_lines.is_empty() && expected_lines.is_empty() { + return JSValue::create_empty_array(global, 0); + } + + let a: Vec<&[u8]> = actual_lines.iter().map(Vec::as_slice).collect(); + let e: Vec<&[u8]> = expected_lines.iter().map(Vec::as_slice).collect(); + + let diff: MyersDiff::DiffList<&[u8]> = if check_comma_disparity { + MyersDiff::Differ::<&[u8], true>::diff(&a, &e).map_err(|err| map_diff_error(global, err))? + } else { + MyersDiff::Differ::<&[u8], false>::diff(&a, &e).map_err(|err| map_diff_error(global, err))? + }; + diff_list_to_js(global, &diff) +} + +fn collect_utf8_elements(global: &JSGlobalObject, array: JSValue) -> JsResult>> { + let length = array.get_length(global)?; + let mut lines = Vec::with_capacity(length as usize); + for index in 0..length { + let element = array.get_index(global, index as u32)?; + let element = bun_core::OwnedString::new(element.to_bun_string(global)?); + lines.push(element.to_utf8_bytes()); + } + Ok(lines) +} + fn diff_chars(global: &JSGlobalObject, actual: &[T], expected: &[T]) -> JsResult where T: Line + FromAny, diff --git a/src/runtime/node/node_assert_binding.rs b/src/runtime/node/node_assert_binding.rs index 50ed81af2da2..6a1c28811ed3 100644 --- a/src/runtime/node/node_assert_binding.rs +++ b/src/runtime/node/node_assert_binding.rs @@ -10,7 +10,7 @@ use super::node_assert; /// Equal = 2, /// } /// type Diff = { operation: DiffType, text: string }; -/// declare function myersDiff(actual: string, expected: string): Diff[]; +/// declare function myersDiff(actual: string | string[], expected: string | string[]): Diff[]; /// ``` #[bun_jsc::host_fn] pub(crate) fn myers_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { @@ -28,6 +28,22 @@ pub(crate) fn myers_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult _ => (frame.argument(2).is_truthy(), frame.argument(3).is_truthy()), }; + // `util.diff()` also diffs arrays of strings, one element per line. + if actual_arg.is_array() || expected_arg.is_array() { + if !actual_arg.is_array() { + return Err(global.throw_invalid_argument_type_value("actual", "array", actual_arg)); + } + if !expected_arg.is_array() { + return Err(global.throw_invalid_argument_type_value("expected", "array", expected_arg)); + } + return node_assert::myers_diff_arrays( + global, + actual_arg, + expected_arg, + check_comma_disparity, + ); + } + if !actual_arg.is_string() { return Err(global.throw_invalid_argument_type_value("actual", "string", actual_arg)); } From a73a01ca1364b9a5a1df39a88951c95510dfc059 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:01:08 +0000 Subject: [PATCH 3/8] dns: emit 'dns' performance entries node records a PerformanceEntry of type 'dns' for every successful lookup, lookupService and resolver query, with the query entries named after the c-ares binding (queryAny, queryA, ...). Bun recorded none, so a PerformanceObserver watching { type: 'dns' } never fired. Entries are only constructed when such an observer is registered. --- src/js/node/dns.ts | 288 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 241 insertions(+), 47 deletions(-) diff --git a/src/js/node/dns.ts b/src/js/node/dns.ts index 7699b1d2b56c..dd34d8a922f5 100644 --- a/src/js/node/dns.ts +++ b/src/js/node/dns.ts @@ -2,7 +2,7 @@ const dns = Bun.dns; const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom"); const { isIP } = require("internal/net/isIP"); -const { guardCallback } = require("internal/shared"); +const { guardCallback, hasObserver, startPerf, stopPerf } = require("internal/shared"); const { validateFunction, validateArray, @@ -269,6 +269,85 @@ function translateLookupOptions(options) { }; } +// node reports a 'dns' performance entry for every successful lookup, +// lookupService and resolver query. Resolver entries are named after the +// c-ares binding (queryAny, queryA, ...) rather than the JS method. +// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/dns/callback_resolver.js#L38-L81 +const kPerfHooksDnsContext = Symbol("kPerfHooksDnsContext"); + +const kQueryBindingNames = { + __proto__: null, + A: "queryA", + AAAA: "queryAaaa", + ANY: "queryAny", + CAA: "queryCaa", + CNAME: "queryCname", + MX: "queryMx", + NAPTR: "queryNaptr", + NS: "queryNs", + PTR: "queryPtr", + SOA: "querySoa", + SRV: "querySrv", + TLSA: "queryTlsa", + TXT: "queryTxt", +}; + +function startDnsPerf(name, detail) { + if (!hasObserver("dns")) return undefined; + const context = { __proto__: null }; + startPerf(context, kPerfHooksDnsContext, { type: "dns", name, detail }); + return context; +} + +function stopDnsPerf(context, detail) { + if (context !== undefined) { + stopPerf(context, kPerfHooksDnsContext, { detail }); + } +} + +function startQueryPerf(rrtype, hostname, ttl) { + const name = kQueryBindingNames[rrtype]; + if (name === undefined) return undefined; + return startDnsPerf(name, { host: hostname, ttl: !!ttl }); +} + +function withQueryPerf(rrtype, hostname, ttl, promise) { + const perf = startQueryPerf(rrtype, hostname, ttl); + if (perf === undefined) return promise; + return promise.then(result => { + stopDnsPerf(perf, { result }); + return result; + }); +} + +function withLookupPerf(hostname, options, promise) { + const perf = startDnsPerf("lookup", lookupPerfDetail(hostname, options)); + if (perf === undefined) return promise; + return promise.then(addresses => { + stopDnsPerf(perf, { addresses: $isJSArray(addresses) ? addresses : [addresses] }); + return addresses; + }); +} + +function withLookupServicePerf(address, port, promise) { + const perf = startDnsPerf("lookupService", { host: address, port }); + if (perf === undefined) return promise; + return promise.then(result => { + stopDnsPerf(perf, { hostname: result.hostname, service: result.service }); + return result; + }); +} + +function lookupPerfDetail(hostname, options) { + return { + hostname, + family: options.family || 0, + hints: options.flags || 0, + verbatim: options.order === "verbatim", + order: options.order, + }; +} + function validateLookupOptions(options) { validateFlagsOption(options); validateFamilyOption(options); @@ -320,6 +399,7 @@ function lookup(hostname, options, callback) { } callback = guardCallback(callback); + const perf = startDnsPerf("lookup", lookupPerfDetail(hostname, options)); dns .lookup(hostname, options) .then(res => { @@ -332,10 +412,13 @@ function lookup(hostname, options, callback) { } if (options?.all) { - callback(null, res.map(mapLookupAll)); + const addresses = res.map(mapLookupAll); + callback(null, addresses); + stopDnsPerf(perf, { addresses }); } else { const [{ address, family }] = res; callback(null, address, family); + stopDnsPerf(perf, { addresses: [{ address, family }] }); } }) .catch(err => { @@ -364,9 +447,11 @@ function lookupService(address, port, callback) { validatePort(port, "port"); callback = guardCallback(callback); + const perf = startDnsPerf("lookupService", { host: address, port: +port }); dns.lookupService(address, +port).then( results => { callback(null, ...results); + stopDnsPerf(perf, { hostname: results[0], service: results[1] }); }, error => { callback(withTranslatedError(error)); @@ -412,6 +497,7 @@ var InternalResolver = class Resolver { callback = validateResolve(hostname, callback); + const perf = startQueryPerf(rrtype?.toUpperCase(), hostname, false); Resolver.#getResolver(this) .resolve(hostname, rrtype) .then( @@ -419,12 +505,11 @@ var InternalResolver = class Resolver { switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - callback(null, results.map(mapResolveX)); - break; - default: - callback(null, results); + results = results.map(mapResolveX); break; } + callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -440,11 +525,14 @@ var InternalResolver = class Resolver { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("A", hostname, options?.ttl); Resolver.#getResolver(this) .resolve(hostname, "A") .then( addresses => { - callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + const result = options?.ttl ? addresses : addresses.map(mapResolveX); + callback(null, result); + stopDnsPerf(perf, { result }); }, error => { callback(withTranslatedError(error)); @@ -460,11 +548,14 @@ var InternalResolver = class Resolver { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("AAAA", hostname, options?.ttl); Resolver.#getResolver(this) .resolve(hostname, "AAAA") .then( addresses => { - callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + const result = options?.ttl ? addresses : addresses.map(mapResolveX); + callback(null, result); + stopDnsPerf(perf, { result }); }, error => { callback(withTranslatedError(error)); @@ -475,11 +566,13 @@ var InternalResolver = class Resolver { resolveAny(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("ANY", hostname, false); Resolver.#getResolver(this) .resolveAny(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -490,11 +583,13 @@ var InternalResolver = class Resolver { resolveCname(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("CNAME", hostname, false); Resolver.#getResolver(this) .resolveCname(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -505,11 +600,13 @@ var InternalResolver = class Resolver { resolveMx(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("MX", hostname, false); Resolver.#getResolver(this) .resolveMx(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -520,11 +617,13 @@ var InternalResolver = class Resolver { resolveNaptr(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("NAPTR", hostname, false); Resolver.#getResolver(this) .resolveNaptr(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -535,11 +634,13 @@ var InternalResolver = class Resolver { resolveNs(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("NS", hostname, false); Resolver.#getResolver(this) .resolveNs(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -550,11 +651,13 @@ var InternalResolver = class Resolver { resolvePtr(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("PTR", hostname, false); Resolver.#getResolver(this) .resolvePtr(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -565,11 +668,13 @@ var InternalResolver = class Resolver { resolveSrv(hostname, callback) { callback = validateResolve(hostname, callback); + const perf = startQueryPerf("SRV", hostname, false); Resolver.#getResolver(this) .resolveSrv(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -583,11 +688,13 @@ var InternalResolver = class Resolver { } callback = guardCallback(callback); + const perf = startQueryPerf("CAA", hostname, false); Resolver.#getResolver(this) .resolveCaa(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -601,11 +708,13 @@ var InternalResolver = class Resolver { } callback = guardCallback(callback); + const perf = startQueryPerf("TXT", hostname, false); Resolver.#getResolver(this) .resolveTxt(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -618,11 +727,13 @@ var InternalResolver = class Resolver { } callback = guardCallback(callback); + const perf = startQueryPerf("SOA", hostname, false); Resolver.#getResolver(this) .resolveSoa(hostname) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -636,11 +747,13 @@ var InternalResolver = class Resolver { } callback = guardCallback(callback); + const perf = startDnsPerf("getHostByAddr", { host: ip, ttl: false }); Resolver.#getResolver(this) .reverse(ip) .then( results => { callback(null, results); + stopDnsPerf(perf, { result: results }); }, error => { callback(withTranslatedError(error)); @@ -769,9 +882,17 @@ const promises = { } if (options.all) { - return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookupAll(options.order))); + return withLookupPerf( + hostname, + options, + translateErrorCode(dns.lookup(hostname, options).then(promisifyLookupAll(options.order))), + ); } - return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookup(options.order))); + return withLookupPerf( + hostname, + options, + translateErrorCode(dns.lookup(hostname, options).then(promisifyLookup(options.order))), + ); }, lookupService(address, port) { @@ -783,10 +904,14 @@ const promises = { validatePort(port, "port"); try { - return translateErrorCode(dns.lookupService(address, +port)).then(([hostname, service]) => ({ - hostname, - service, - })); + return withLookupServicePerf( + address, + +port, + translateErrorCode(dns.lookupService(address, +port)).then(([hostname, service]) => ({ + hostname, + service, + })), + ); } catch (err) { if (err.name === "TypeError" || err.name === "RangeError") { throw err; @@ -809,52 +934,67 @@ const promises = { switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyResolveX(false))); + return withQueryPerf( + rrtype.toUpperCase(), + hostname, + false, + translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyResolveX(false))), + ); default: - return translateErrorCode(dns.resolve(hostname, rrtype)); + return withQueryPerf(rrtype?.toUpperCase(), hostname, false, translateErrorCode(dns.resolve(hostname, rrtype))); } }, resolve4(hostname, options) { - return translateErrorCode(dns.resolve(hostname, "A").then(promisifyResolveX(options?.ttl))); + return withQueryPerf( + "A", + hostname, + options?.ttl, + translateErrorCode(dns.resolve(hostname, "A").then(promisifyResolveX(options?.ttl))), + ); }, resolve6(hostname, options) { - return translateErrorCode(dns.resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl))); + return withQueryPerf( + "AAAA", + hostname, + options?.ttl, + translateErrorCode(dns.resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl))), + ); }, resolveAny(hostname) { - return translateErrorCode(dns.resolveAny(hostname)); + return withQueryPerf("ANY", hostname, false, translateErrorCode(dns.resolveAny(hostname))); }, resolveSrv(hostname) { - return translateErrorCode(dns.resolveSrv(hostname)); + return withQueryPerf("SRV", hostname, false, translateErrorCode(dns.resolveSrv(hostname))); }, resolveTxt(hostname) { - return translateErrorCode(dns.resolveTxt(hostname)); + return withQueryPerf("TXT", hostname, false, translateErrorCode(dns.resolveTxt(hostname))); }, resolveSoa(hostname) { - return translateErrorCode(dns.resolveSoa(hostname)); + return withQueryPerf("SOA", hostname, false, translateErrorCode(dns.resolveSoa(hostname))); }, resolveNaptr(hostname) { - return translateErrorCode(dns.resolveNaptr(hostname)); + return withQueryPerf("NAPTR", hostname, false, translateErrorCode(dns.resolveNaptr(hostname))); }, resolveMx(hostname) { - return translateErrorCode(dns.resolveMx(hostname)); + return withQueryPerf("MX", hostname, false, translateErrorCode(dns.resolveMx(hostname))); }, resolveCaa(hostname) { - return translateErrorCode(dns.resolveCaa(hostname)); + return withQueryPerf("CAA", hostname, false, translateErrorCode(dns.resolveCaa(hostname))); }, resolveNs(hostname) { - return translateErrorCode(dns.resolveNs(hostname)); + return withQueryPerf("NS", hostname, false, translateErrorCode(dns.resolveNs(hostname))); }, resolvePtr(hostname) { - return translateErrorCode(dns.resolvePtr(hostname)); + return withQueryPerf("PTR", hostname, false, translateErrorCode(dns.resolvePtr(hostname))); }, resolveCname(hostname) { - return translateErrorCode(dns.resolveCname(hostname)); + return withQueryPerf("CNAME", hostname, false, translateErrorCode(dns.resolveCname(hostname))); }, reverse(ip) { - return translateErrorCode(dns.reverse(ip)); + return withQueryPerf("PTR", ip, false, translateErrorCode(dns.reverse(ip))); }, Resolver: class Resolver { @@ -885,68 +1025,122 @@ const promises = { switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode( - Resolver.#getResolver(this).resolve(hostname, rrtype).then(promisifyResolveX(false)), + return withQueryPerf( + rrtype.toUpperCase(), + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype).then(promisifyResolveX(false))), ); default: - return translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype)); + return withQueryPerf( + rrtype?.toUpperCase(), + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype)), + ); } } resolve4(hostname, options) { - return translateErrorCode( - Resolver.#getResolver(this).resolve(hostname, "A").then(promisifyResolveX(options?.ttl)), + return withQueryPerf( + "A", + hostname, + options?.ttl, + translateErrorCode(Resolver.#getResolver(this).resolve(hostname, "A").then(promisifyResolveX(options?.ttl))), ); } resolve6(hostname, options) { - return translateErrorCode( - Resolver.#getResolver(this).resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl)), + return withQueryPerf( + "AAAA", + hostname, + options?.ttl, + translateErrorCode(Resolver.#getResolver(this).resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl))), ); } resolveAny(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveAny(hostname)); + return withQueryPerf( + "ANY", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveAny(hostname)), + ); } resolveCname(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveCname(hostname)); + return withQueryPerf( + "CNAME", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveCname(hostname)), + ); } resolveMx(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveMx(hostname)); + return withQueryPerf("MX", hostname, false, translateErrorCode(Resolver.#getResolver(this).resolveMx(hostname))); } resolveNaptr(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveNaptr(hostname)); + return withQueryPerf( + "NAPTR", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveNaptr(hostname)), + ); } resolveNs(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveNs(hostname)); + return withQueryPerf("NS", hostname, false, translateErrorCode(Resolver.#getResolver(this).resolveNs(hostname))); } resolvePtr(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolvePtr(hostname)); + return withQueryPerf( + "PTR", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolvePtr(hostname)), + ); } resolveSoa(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveSoa(hostname)); + return withQueryPerf( + "SOA", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveSoa(hostname)), + ); } resolveSrv(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveSrv(hostname)); + return withQueryPerf( + "SRV", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveSrv(hostname)), + ); } resolveCaa(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveCaa(hostname)); + return withQueryPerf( + "CAA", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveCaa(hostname)), + ); } resolveTxt(hostname) { - return translateErrorCode(Resolver.#getResolver(this).resolveTxt(hostname)); + return withQueryPerf( + "TXT", + hostname, + false, + translateErrorCode(Resolver.#getResolver(this).resolveTxt(hostname)), + ); } reverse(ip) { - return translateErrorCode(Resolver.#getResolver(this).reverse(ip)); + return withQueryPerf("PTR", ip, false, translateErrorCode(Resolver.#getResolver(this).reverse(ip))); } setLocalAddress(first, second) { From 926e92b1b3b3934b0579dd0eed32aefc42f52c23 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:23:47 +0000 Subject: [PATCH 4/8] test: vendor node v26.3.0 tests for util.diff and dns perf entries test-diff.js and test-dns-perf_hooks.js are unblocked by the two preceding commits. test-compile-cache-typescript-esm.js already passed (it skips itself on a build that reports no Amaro, which is what Bun reports). --- .../test-compile-cache-typescript-esm.js | 170 ++++++++++++++++++ test/js/node/test/parallel/test-diff.js | 80 +++++++++ .../node/test/parallel/test-dns-perf_hooks.js | 61 +++++++ 3 files changed, 311 insertions(+) create mode 100644 test/js/node/test/parallel/test-compile-cache-typescript-esm.js create mode 100644 test/js/node/test/parallel/test-diff.js create mode 100644 test/js/node/test/parallel/test-dns-perf_hooks.js diff --git a/test/js/node/test/parallel/test-compile-cache-typescript-esm.js b/test/js/node/test/parallel/test-compile-cache-typescript-esm.js new file mode 100644 index 000000000000..98473dbb2a5b --- /dev/null +++ b/test/js/node/test/parallel/test-compile-cache-typescript-esm.js @@ -0,0 +1,170 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works for ESM with types. + +const common = require('../common'); +if (!process.config.variables.node_use_amaro) { + common.skip('Requires Amaro'); +} +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +// Check cache for .ts files that would be run as ESM. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'ts', 'test-module-typescript.ts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-module-typescript\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); + assert.match(output, /writing cache for ESM .*test-module-typescript\.ts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for ESM .*test-module-typescript\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-module-typescript\.ts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-module-typescript\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-module-typescript\.ts because cache was the same/); + return true; + } + }); +} + +// Check cache for .mts files that import .mts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'mts', 'test-import-module.mts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-import-module\.mts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /writing cache for ESM .*test-import-module\.mts.*success/); + assert.match(output, /writing cache for ESM .*test-mts-export-foo\.mts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-module\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-import-module\.mts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-mts-export-foo\.mts because cache was the same/); + + assert.match(output, /V8 code cache for ESM .*test-import-module\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-import-module\.mts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-mts-export-foo\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-mts-export-foo\.mts because cache was the same/); + return true; + } + }); +} + + +// Check cache for .mts files that import .cts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'mts', 'test-import-commonjs.mts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /writing cache for ESM .*test-import-commonjs\.mts.*success/); + assert.match(output, /writing cache for CommonJS .*test-cts-export-foo\.cts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-import-commonjs\.mts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-cts-export-foo\.cts because cache was the same/); + + assert.match(output, /V8 code cache for ESM .*test-import-commonjs\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-import-commonjs\.mts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-cts-export-foo\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-cts-export-foo\.cts because cache was the same/); + return true; + } + }); +} diff --git a/test/js/node/test/parallel/test-diff.js b/test/js/node/test/parallel/test-diff.js new file mode 100644 index 000000000000..dfc7027a3ed9 --- /dev/null +++ b/test/js/node/test/parallel/test-diff.js @@ -0,0 +1,80 @@ +'use strict'; +require('../common'); + +const { describe, it } = require('node:test'); +const assert = require('node:assert'); + +const { diff } = require('util'); + +describe('diff', () => { + it('throws because actual is nor an array nor a string', () => { + const actual = {}; + const expected = 'foo'; + + assert.throws(() => diff(actual, expected), { + message: 'The "actual" argument must be of type string. Received an instance of Object' + }); + }); + + it('throws because expected is nor an array nor a string', () => { + const actual = 'foo'; + const expected = {}; + + assert.throws(() => diff(actual, expected), { + message: 'The "expected" argument must be of type string. Received an instance of Object' + }); + }); + + + it('throws because the actual array does not only contain string', () => { + const actual = ['1', { b: 2 }]; + const expected = ['1', '2']; + + assert.throws(() => diff(actual, expected), { + message: 'The "actual[1]" argument must be of type string. Received an instance of Object' + }); + }); + + it('returns an empty array because actual and expected are the same', () => { + const actual = 'foo'; + const expected = 'foo'; + + const result = diff(actual, expected); + assert.deepStrictEqual(result, []); + }); + + it('returns the diff for strings', () => { + const actual = '12345678'; + const expected = '12!!5!7!'; + const result = diff(actual, expected); + + assert.deepStrictEqual(result, [ + [0, '1'], + [0, '2'], + [1, '3'], + [1, '4'], + [-1, '!'], + [-1, '!'], + [0, '5'], + [1, '6'], + [-1, '!'], + [0, '7'], + [1, '8'], + [-1, '!'], + ]); + }); + + it('returns the diff for arrays', () => { + const actual = ['1', '2', '3']; + const expected = ['1', '3', '4']; + const result = diff(actual, expected); + + assert.deepStrictEqual(result, [ + [0, '1'], + [1, '2'], + [0, '3'], + [-1, '4'], + ] + ); + }); +}); diff --git a/test/js/node/test/parallel/test-dns-perf_hooks.js b/test/js/node/test/parallel/test-dns-perf_hooks.js new file mode 100644 index 000000000000..694b2e77249e --- /dev/null +++ b/test/js/node/test/parallel/test-dns-perf_hooks.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const dns = require('dns'); +const { PerformanceObserver } = require('perf_hooks'); + +const entries = []; +const obs = new PerformanceObserver((items) => { + entries.push(...items.getEntries()); +}); + +obs.observe({ type: 'dns' }); + +let count = 0; + +function inc() { + count++; +} + +// If DNS resolution fails, skip it +// https://github.com/nodejs/node/issues/44003 +dns.lookup('localhost', common.mustCall((err) => { !err && inc(); })); +dns.lookupService('127.0.0.1', 80, common.mustCall((err) => { !err && inc(); })); +dns.resolveAny('localhost', common.mustCall((err) => { !err && inc(); })); + +dns.promises.lookup('localhost').then(inc).catch(() => {}); +dns.promises.lookupService('127.0.0.1', 80).then(inc).catch(() => {}); +dns.promises.resolveAny('localhost').then(inc).catch(() => {}); + +process.on('exit', () => { + assert.strictEqual(entries.length, count); + entries.forEach((entry) => { + assert.strictEqual(!!entry.name, true); + assert.strictEqual(entry.entryType, 'dns'); + assert.strictEqual(typeof entry.startTime, 'number'); + assert.strictEqual(typeof entry.duration, 'number'); + assert.strictEqual(typeof entry.detail, 'object'); + switch (entry.name) { + case 'lookup': + assert.strictEqual(typeof entry.detail.hostname, 'string'); + assert.strictEqual(typeof entry.detail.family, 'number'); + assert.strictEqual(typeof entry.detail.hints, 'number'); + assert.strictEqual(typeof entry.detail.verbatim, 'boolean'); + assert.strictEqual(typeof entry.detail.order, 'string'); + assert.strictEqual(Array.isArray(entry.detail.addresses), true); + break; + case 'lookupService': + assert.strictEqual(typeof entry.detail.host, 'string'); + assert.strictEqual(typeof entry.detail.port, 'number'); + assert.strictEqual(typeof entry.detail.hostname, 'string'); + assert.strictEqual(typeof entry.detail.service, 'string'); + break; + case 'queryAny': + assert.strictEqual(typeof entry.detail.host, 'string'); + assert.strictEqual(typeof entry.detail.ttl, 'boolean'); + assert.strictEqual(Array.isArray(entry.detail.result), true); + break; + } + }); +}); From 128667f539054a7dea61cd68c64965ec7285fe66 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:35:02 +0000 Subject: [PATCH 5/8] assert: reject WeakMap/WeakSet/Promise on the left operand only deepStrictEqual bailed out when either side was a WeakMap, WeakSet or Promise. node tests only the left operand, which makes the comparison asymmetric on purpose: a Proxy wrapping a promise equals that promise with the proxy on the left, but not the other way round. Verified against the v26.3.0 binary. --- src/jsc/bindings/bindings.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index bfbcdd521508..d15413e92ab1 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -814,6 +814,18 @@ bool Bun__deepEquals(JSC::JSGlobalObject* globalObject, JSValue v1, JSValue v2, } } + if constexpr (checkPrototypes) { + // Distinct WeakMaps, WeakSets and Promises are never equal - their + // contents cannot be inspected. node tests this on the LEFT operand + // only, which makes the comparison asymmetric: a Proxy wrapping a + // promise equals that promise, but only with the proxy on the left. + // https://github.com/nodejs/node/blob/v26.3.0/lib/internal/util/comparisons.js#L449-L451 + uint8_t leftType = c1->type(); + if (leftType == JSC::JSWeakMapType || leftType == JSC::JSWeakSetType || leftType == JSC::JSPromiseType) { + return false; + } + } + std::optional isSpecialEqual = specialObjectsDequal(globalObject, gcBuffer, stack, scope, c1, c2); RETURN_IF_EXCEPTION(scope, false); if (isSpecialEqual.has_value()) return WTF::move(*isSpecialEqual); @@ -1778,12 +1790,6 @@ std::optional specialObjectsDequal(JSC::JSGlobalObject* globalObject, Mark // Symbol/BigInt wrapper objects have no dedicated JSType; node compares // their internal values. Only for the node entry point. if constexpr (checkPrototypes) { - // node never considers distinct WeakMaps, WeakSets, or Promises equal - // (their contents cannot be inspected). - if (c1Type == JSC::JSWeakMapType || c1Type == JSC::JSWeakSetType || c1Type == JSC::JSPromiseType - || c2Type == JSC::JSWeakMapType || c2Type == JSC::JSWeakSetType || c2Type == JSC::JSPromiseType) { - return false; - } auto* symbolObject1 = dynamicDowncast(c1); auto* symbolObject2 = dynamicDowncast(c2); if (symbolObject1 || symbolObject2) { From 5d20953e7b5e2a700bcc8b6863d256590885a97d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:35:02 +0000 Subject: [PATCH 6/8] test: vendor node v26.3.0 test-common-must-not-mutate-object-deep Exercises deepStrictEqual against the immutable-view proxies the upstream harness hands to tests. --- ...est-common-must-not-mutate-object-deep.mjs | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 test/js/node/test/parallel/test-common-must-not-mutate-object-deep.mjs diff --git a/test/js/node/test/parallel/test-common-must-not-mutate-object-deep.mjs b/test/js/node/test/parallel/test-common-must-not-mutate-object-deep.mjs new file mode 100644 index 000000000000..926a03870562 --- /dev/null +++ b/test/js/node/test/parallel/test-common-must-not-mutate-object-deep.mjs @@ -0,0 +1,225 @@ +import { mustNotMutateObjectDeep } from '../common/index.mjs'; +import assert from 'node:assert'; +import { promisify } from 'node:util'; + +// Test common.mustNotMutateObjectDeep() + +const original = { + foo: { bar: 'baz' }, + qux: null, + quux: [ + 'quuz', + { corge: 'grault' }, + ], +}; + +// Make a copy to make sure original doesn't get altered by the function itself. +const backup = structuredClone(original); + +// Wrapper for convenience: +const obj = () => mustNotMutateObjectDeep(original); + +function testOriginal(root) { + assert.deepStrictEqual(root, backup); + return root.foo.bar === 'baz' && root.quux[1].corge.length === 6; +} + +function definePropertyOnRoot(root) { + Object.defineProperty(root, 'xyzzy', {}); +} + +function definePropertyOnFoo(root) { + Object.defineProperty(root.foo, 'xyzzy', {}); +} + +function deletePropertyOnRoot(root) { + delete root.foo; +} + +function deletePropertyOnFoo(root) { + delete root.foo.bar; +} + +function preventExtensionsOnRoot(root) { + Object.preventExtensions(root); +} + +function preventExtensionsOnFoo(root) { + Object.preventExtensions(root.foo); +} + +function preventExtensionsOnRootViaSeal(root) { + Object.seal(root); +} + +function preventExtensionsOnFooViaSeal(root) { + Object.seal(root.foo); +} + +function preventExtensionsOnRootViaFreeze(root) { + Object.freeze(root); +} + +function preventExtensionsOnFooViaFreeze(root) { + Object.freeze(root.foo); +} + +function setOnRoot(root) { + root.xyzzy = 'gwak'; +} + +function setOnFoo(root) { + root.foo.xyzzy = 'gwak'; +} + +function setQux(root) { + root.qux = 'gwak'; +} + +function setQuux(root) { + root.quux.push('gwak'); +} + +function setQuuxItem(root) { + root.quux[0] = 'gwak'; +} + +function setQuuxProperty(root) { + root.quux[1].corge = 'gwak'; +} + +function setPrototypeOfRoot(root) { + Object.setPrototypeOf(root, Array); +} + +function setPrototypeOfFoo(root) { + Object.setPrototypeOf(root.foo, Array); +} + +function setPrototypeOfQuux(root) { + Object.setPrototypeOf(root.quux, Array); +} + + +{ + assert.ok(testOriginal(obj())); + + assert.throws( + () => definePropertyOnRoot(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => definePropertyOnFoo(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => deletePropertyOnRoot(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => deletePropertyOnFoo(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnRoot(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnFoo(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnRootViaSeal(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnFooViaSeal(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnRootViaFreeze(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => preventExtensionsOnFooViaFreeze(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setOnRoot(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setOnFoo(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setQux(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setQuux(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setQuux(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setQuuxItem(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setQuuxProperty(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setPrototypeOfRoot(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setPrototypeOfFoo(obj()), + { code: 'ERR_ASSERTION' } + ); + assert.throws( + () => setPrototypeOfQuux(obj()), + { code: 'ERR_ASSERTION' } + ); + + // Test that no mutation happened: + assert.ok(testOriginal(obj())); +} + +// Test various supported types, directly and nested: +[ + undefined, null, false, true, 42, 42n, Symbol('42'), NaN, Infinity, {}, [], + () => {}, async () => {}, Promise.resolve(), Math, { __proto__: null }, +].forEach((target) => { + assert.deepStrictEqual(mustNotMutateObjectDeep(target), target); + assert.deepStrictEqual(mustNotMutateObjectDeep({ target }), { target }); + assert.deepStrictEqual(mustNotMutateObjectDeep([ target ]), [ target ]); +}); + +// Test that passed functions keep working correctly: +{ + const fn = () => 'blep'; + fn.foo = {}; + const fnImmutableView = mustNotMutateObjectDeep(fn); + assert.deepStrictEqual(fnImmutableView, fn); + + // Test that the function still works: + assert.strictEqual(fn(), 'blep'); + assert.strictEqual(fnImmutableView(), 'blep'); + + // Test that the original function is not deeply frozen: + fn.foo.bar = 'baz'; + assert.strictEqual(fn.foo.bar, 'baz'); + assert.strictEqual(fnImmutableView.foo.bar, 'baz'); + + // Test the original function is not frozen: + fn.qux = 'quux'; + assert.strictEqual(fn.qux, 'quux'); + assert.strictEqual(fnImmutableView.qux, 'quux'); + + // Redefining util.promisify.custom also works: + promisify(mustNotMutateObjectDeep(promisify(fn))); +} From 436d89b4ec4febb9c71bda8545575dc31bbb9dc1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 04:39:44 +0000 Subject: [PATCH 7/8] [autofix.ci] apply automated fixes --- src/runtime/node/node_assert.rs | 3 ++- src/runtime/node/node_assert_binding.rs | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/runtime/node/node_assert.rs b/src/runtime/node/node_assert.rs index 5dd255b55ba9..14e2649774aa 100644 --- a/src/runtime/node/node_assert.rs +++ b/src/runtime/node/node_assert.rs @@ -107,7 +107,8 @@ pub(crate) fn myers_diff_arrays( let diff: MyersDiff::DiffList<&[u8]> = if check_comma_disparity { MyersDiff::Differ::<&[u8], true>::diff(&a, &e).map_err(|err| map_diff_error(global, err))? } else { - MyersDiff::Differ::<&[u8], false>::diff(&a, &e).map_err(|err| map_diff_error(global, err))? + MyersDiff::Differ::<&[u8], false>::diff(&a, &e) + .map_err(|err| map_diff_error(global, err))? }; diff_list_to_js(global, &diff) } diff --git a/src/runtime/node/node_assert_binding.rs b/src/runtime/node/node_assert_binding.rs index 6a1c28811ed3..d65570733411 100644 --- a/src/runtime/node/node_assert_binding.rs +++ b/src/runtime/node/node_assert_binding.rs @@ -34,7 +34,11 @@ pub(crate) fn myers_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult return Err(global.throw_invalid_argument_type_value("actual", "array", actual_arg)); } if !expected_arg.is_array() { - return Err(global.throw_invalid_argument_type_value("expected", "array", expected_arg)); + return Err(global.throw_invalid_argument_type_value( + "expected", + "array", + expected_arg, + )); } return node_assert::myers_diff_arrays( global, From 0c71dce4ef3d32285dfe910ab6df2b221bc2c224 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 04:40:42 +0000 Subject: [PATCH 8/8] test: drop test-compile-cache-typescript-esm It only ever skips: it gates on process.config.variables.node_use_amaro, which Bun reports as falsy and cannot report as true. Vendoring it adds a file that asserts nothing. --- .../test-compile-cache-typescript-esm.js | 170 ------------------ 1 file changed, 170 deletions(-) delete mode 100644 test/js/node/test/parallel/test-compile-cache-typescript-esm.js diff --git a/test/js/node/test/parallel/test-compile-cache-typescript-esm.js b/test/js/node/test/parallel/test-compile-cache-typescript-esm.js deleted file mode 100644 index 98473dbb2a5b..000000000000 --- a/test/js/node/test/parallel/test-compile-cache-typescript-esm.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -// This tests NODE_COMPILE_CACHE works for ESM with types. - -const common = require('../common'); -if (!process.config.variables.node_use_amaro) { - common.skip('Requires Amaro'); -} -const { spawnSyncAndAssert } = require('../common/child_process'); -const assert = require('assert'); -const tmpdir = require('../common/tmpdir'); -const fixtures = require('../common/fixtures'); - -// Check cache for .ts files that would be run as ESM. -{ - tmpdir.refresh(); - const dir = tmpdir.resolve('.compile_cache_dir'); - const script = fixtures.path('typescript', 'ts', 'test-module-typescript.ts'); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-module-typescript\.ts/); - assert.match(output, /writing cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); - assert.match(output, /writing cache for ESM .*test-module-typescript\.ts.*success/); - return true; - } - }); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); - assert.match(output, /reading cache from .* for ESM .*test-module-typescript\.ts.*success/); - assert.match(output, /skip persisting StrippedTypeScript .*test-module-typescript\.ts because cache was the same/); - assert.match(output, /V8 code cache for ESM .*test-module-typescript\.ts was accepted, keeping the in-memory entry/); - assert.match(output, /skip persisting ESM .*test-module-typescript\.ts because cache was the same/); - return true; - } - }); -} - -// Check cache for .mts files that import .mts files. -{ - tmpdir.refresh(); - const dir = tmpdir.resolve('.compile_cache_dir'); - const script = fixtures.path('typescript', 'mts', 'test-import-module.mts'); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /writing cache for StrippedTypeScript .*test-import-module\.mts.*success/); - assert.match(output, /writing cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); - assert.match(output, /writing cache for ESM .*test-import-module\.mts.*success/); - assert.match(output, /writing cache for ESM .*test-mts-export-foo\.mts.*success/); - return true; - } - }); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-module\.mts.*success/); - assert.match(output, /skip persisting StrippedTypeScript .*test-import-module\.mts because cache was the same/); - assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); - assert.match(output, /skip persisting StrippedTypeScript .*test-mts-export-foo\.mts because cache was the same/); - - assert.match(output, /V8 code cache for ESM .*test-import-module\.mts was accepted, keeping the in-memory entry/); - assert.match(output, /skip persisting ESM .*test-import-module\.mts because cache was the same/); - assert.match(output, /V8 code cache for ESM .*test-mts-export-foo\.mts was accepted, keeping the in-memory entry/); - assert.match(output, /skip persisting ESM .*test-mts-export-foo\.mts because cache was the same/); - return true; - } - }); -} - - -// Check cache for .mts files that import .cts files. -{ - tmpdir.refresh(); - const dir = tmpdir.resolve('.compile_cache_dir'); - const script = fixtures.path('typescript', 'mts', 'test-import-commonjs.mts'); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /writing cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); - assert.match(output, /writing cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); - assert.match(output, /writing cache for ESM .*test-import-commonjs\.mts.*success/); - assert.match(output, /writing cache for CommonJS .*test-cts-export-foo\.cts.*success/); - return true; - } - }); - - spawnSyncAndAssert( - process.execPath, - [script], - { - env: { - ...process.env, - NODE_DEBUG_NATIVE: 'COMPILE_CACHE', - NODE_COMPILE_CACHE: dir - }, - cwd: tmpdir.path - }, - { - stderr(output) { - assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); - assert.match(output, /skip persisting StrippedTypeScript .*test-import-commonjs\.mts because cache was the same/); - assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); - assert.match(output, /skip persisting StrippedTypeScript .*test-cts-export-foo\.cts because cache was the same/); - - assert.match(output, /V8 code cache for ESM .*test-import-commonjs\.mts was accepted, keeping the in-memory entry/); - assert.match(output, /skip persisting ESM .*test-import-commonjs\.mts because cache was the same/); - assert.match(output, /V8 code cache for CommonJS .*test-cts-export-foo\.cts was accepted, keeping the in-memory entry/); - assert.match(output, /skip persisting CommonJS .*test-cts-export-foo\.cts because cache was the same/); - return true; - } - }); -}