Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions src/js/builtins/ProcessObjectInternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,3 +568,11 @@ export function getChannel() {
}
})();
}

// Called (with `this` = process) by the native promise-rejection tracker so
// 'unhandledRejection' listeners run with a JS frame on the stack: node
// dispatches through internal/process/promises and listeners may call
// Error.captureStackTrace(err, listener) expecting caller frames to remain.
export function emitUnhandledRejectionFromNative(reason, promise) {
return this.emit("unhandledRejection", reason, promise);
}
29 changes: 28 additions & 1 deletion src/js/internal/test/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,35 @@ function internalBinding(name: string) {
return { UDP: require("internal/dgram").UDP };
case "tcp_wrap":
return { TCP: TestTCPWrap, constants: { SOCKET: 0, SERVER: 1 } };
// Just what vendored modules destructure at load; Bun always builds with ICU.
case "config":
return { hasIntl: true };
// node's C++ encoding binding, backed by the runtime's own encoders.
case "encoding_binding": {
const utf8Encoder = new TextEncoder();
const encodeIntoResults = new Uint32Array(2);
return {
encodeInto(source: string, dest: Uint8Array) {
const { read, written } = utf8Encoder.encodeInto(source, dest);
encodeIntoResults[0] = read;
encodeIntoResults[1] = written;
},
encodeIntoResults,
encodeUtf8String(source: string) {
return utf8Encoder.encode(source);
},
decodeUTF8(input: ArrayBufferView, ignoreBOM: boolean, fatal: boolean) {
return new TextDecoder("utf-8", { ignoreBOM, fatal }).decode(input);
},
};
}
case "util":
return { isInsideNodeModules };
return {
isInsideNodeModules,
// node's util binding exposes engine-private symbols; vendored
// internal/errors.js stores its arrow message under this one.
privateSymbols: { arrow_message_private_symbol: Symbol("node:arrowMessage") },
};
// The icu-era binding node exposed until nodejs/node#55156; vendored
// tests like test-icu-punycode still consume it.
case "icu": {
Expand Down
41 changes: 36 additions & 5 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1443,10 +1443,20 @@ extern "C" int Bun__handleUnhandledRejection(JSC::JSGlobalObject* lexicalGlobalO
auto eventType = Identifier::fromString(JSC::getVM(globalObject), "unhandledRejection"_s);
auto& wrapped = process->wrapped();
if (wrapped.listenerCount(eventType) > 0) {
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
// Dispatch through a JS trampoline (which forwards to process.emit and
// the same listener store) so listeners run with JS caller frames, as
// in node; Error.captureStackTrace(err, listener) inside a listener
// must leave a non-empty stack. Listener exceptions are reported by
// the emitter itself, same as the direct wrapped.emit() path.
JSC::JSFunction* emitter = JSC::JSFunction::create(vm, globalObject, processObjectInternalsEmitUnhandledRejectionFromNativeCodeGenerator(vm), globalObject);
MarkedArgumentBuffer args;
args.append(reason);
args.append(promise);
wrapped.emit(eventType, args);
auto callData = JSC::getCallData(emitter);
JSC::profiledCall(globalObject, JSC::ProfilingReason::API, emitter, callData, process, args);
CLEAR_IF_EXCEPTION(scope);
return true;
}

Expand All @@ -1465,10 +1475,6 @@ extern "C" bool Bun__emitHandledPromiseEvent(JSC::JSGlobalObject* lexicalGlobalO

auto eventType = Identifier::fromString(JSC::getVM(globalObject), "rejectionHandled"_s);

if (Bun__VM__allowRejectionHandledWarning(globalObject->bunVM())) {
Process::emitWarning(globalObject, jsString(globalObject->vm(), String("Promise rejection was handled asynchronously"_s)), jsString(globalObject->vm(), String("PromiseRejectionHandledWarning"_s)), jsUndefined(), jsUndefined());
CLEAR_IF_EXCEPTION(scope);
}
auto& wrapped = process->wrapped();
if (wrapped.listenerCount(eventType) > 0) {
MarkedArgumentBuffer args;
Expand All @@ -1477,6 +1483,13 @@ extern "C" bool Bun__emitHandledPromiseEvent(JSC::JSGlobalObject* lexicalGlobalO
return true;
}

// Node only warns when nothing handled the 'rejectionHandled' event
// (processPromiseRejections: `if (!process.emit('rejectionHandled', ...))`).
if (Bun__VM__allowRejectionHandledWarning(globalObject->bunVM())) {
Process::emitWarning(globalObject, jsString(globalObject->vm(), String("Promise rejection was handled asynchronously"_s)), jsString(globalObject->vm(), String("PromiseRejectionHandledWarning"_s)), jsUndefined(), jsUndefined());
CLEAR_IF_EXCEPTION(scope);
}

return false;
}

Expand Down Expand Up @@ -2038,6 +2051,24 @@ JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue w
auto s = warning.getString(globalObject);
errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s);
errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0);
// With no JS frames on the stack (native emission) the created error
// has no `stack` at all; node always produces at least the
// "<name>: <message>" header line and warning handlers read it.
JSValue existingStack = errorInstance->get(globalObject, vm.propertyNames->stack);
RETURN_IF_EXCEPTION(scope, {});
bool stackMissing = existingStack.isUndefinedOrNull();
if (!stackMissing && existingStack.isString()) {
auto stackString = existingStack.getString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
stackMissing = stackString.isEmpty();
}
if (stackMissing) {
auto typeString = type.isString() ? type.getString(globalObject) : String("Warning"_s);
RETURN_IF_EXCEPTION(scope, {});
errorInstance->putDirect(vm, vm.propertyNames->stack,
jsString(vm, makeString(typeString, ": "_s, !s.isEmpty() ? s : String("Warning"_s))),
static_cast<unsigned>(JSC::PropertyAttribute::DontEnum));
}
} else if (warning.isCell() && warning.asCell()->type() == ErrorInstanceType) {
errorInstance = warning.getObject();
} else {
Expand Down
6 changes: 4 additions & 2 deletions src/jsc/bindings/webcore/streams/WebStreamsInspectCustom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ WTF::String constructorNameOf(JSGlobalObject* lexicalGlobalObject, JSValue thisV
void installInspectCustom(VM& vm, JSObject* prototype, NativeFunction nativeFunction)
{
auto* globalObject = prototype->globalObject();
prototype->putDirectNativeFunction(vm, globalObject, WebCore::builtinNames(vm).inspectCustomPublicName(), 2,
nativeFunction, ImplementationVisibility::Public, NoIntrinsic,
// Node names this method "[nodejs.util.inspect.custom]" (V8's symbol-keyed
// method naming); user code and node's own tests read fn.name.
auto* function = JSFunction::create(vm, globalObject, 2, "[nodejs.util.inspect.custom]"_s, nativeFunction, ImplementationVisibility::Public);
prototype->putDirect(vm, WebCore::builtinNames(vm).inspectCustomPublicName(), function,
static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum));
}

Expand Down
7 changes: 5 additions & 2 deletions test/js/node/test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ if (process.argv.length === 2 &&
const { onGCSweepSync } = require('./gc');
const { releaseWeakRefs } = require('bun:jsc');
globalThis.gc ??= () => { Bun.gc(true); onGCSweepSync(releaseWeakRefs, Bun.gc); };
break;
// Keep scanning: a later --expose-internals on the same Flags line
// (e.g. `--expose-gc --expose-internals`) still needs its shim.
continue;
}
if ((flag === "--expose-externalize-string" || flag === "--expose_externalize_string") && process.versions.bun) {
// V8's externalized-string test helpers. JavaScriptCore has no string
Expand All @@ -189,7 +191,8 @@ if (process.argv.length === 2 &&
}
return true;
};
break;
// Keep scanning for the same reason as --expose-gc above.
continue;
}
if ((flag === "--experimental-sqlite" || flag === "--no-experimental-sqlite") && process.versions.bun) {
// node:sqlite is always available in Bun; the Node experimental gate
Expand Down
26 changes: 25 additions & 1 deletion test/js/node/test/common/nodeinternals.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const path = require('path');
const util = require('util');

const VENDORED = new Set([
'internal/encoding',
'internal/encoding/single-byte',
'internal/encoding/util',
'internal/errors',
'internal/webidl',
'internal/socket_list',
'internal/fs/utils',
Expand All @@ -18,7 +22,7 @@ const VENDORED = new Set([

// ---------------- primordials emulator ----------------
const globalsMap = {
Array, ArrayBuffer, BigInt, Boolean, DataView, Date, Error, EvalError,
AggregateError, Array, ArrayBuffer, BigInt, Boolean, DataView, Date, Error, EvalError,
FinalizationRegistry, Function, JSON, Map, Math, Number, Object, Promise,
Proxy, RangeError, ReferenceError, Reflect, RegExp, Set, String, Symbol,
SyntaxError, TypeError, URIError, WeakMap, WeakRef, WeakSet,
Expand Down Expand Up @@ -112,6 +116,13 @@ function computePrimordial(name) {
if (name.startsWith('TypedArrayPrototype')) {
return resolveOnProto(TypedArray.prototype, name.slice('TypedArrayPrototype'.length), name);
}
if (name.startsWith('TypedArray')) {
// %TypedArray% statics are uncurried over the concrete constructor:
// TypedArrayOf(Uint16Array, 1, 2) === Uint16Array.of(1, 2).
const method = lowerFirst(name.slice('TypedArray'.length));
if (typeof TypedArray[method] === 'function') return uncurryThis(TypedArray[method]);
throw new Error(`nodeinternals primordials: cannot resolve ${name}`);
}

for (const g of Object.keys(globalsMap)) {
if (name === g) return globalsMap[g];
Expand Down Expand Up @@ -455,6 +466,7 @@ function getOverrides() {
return result;
};
},
customInspectSymbol: Symbol.for('nodejs.util.inspect.custom'),
isWindows: process.platform === 'win32',
deprecate: util.deprecate,
lazyDOMException: (message, name) => new DOMException(message, name),
Expand Down Expand Up @@ -498,6 +510,18 @@ function getOverrides() {
UVException,
},
'internal/util': iuExtended,
'internal/buffer': (() => {
class FastBuffer extends Uint8Array {
constructor(bufferOrLength, byteOffset, length) {
if (bufferOrLength === undefined) super(0);
else if (typeof bufferOrLength === 'number') super(bufferOrLength);
else super(bufferOrLength, byteOffset, length);
}
}
FastBuffer.prototype.constructor = Buffer;
Object.setPrototypeOf(FastBuffer.prototype, Buffer.prototype);
return { FastBuffer };
})(),
'internal/util/types': require('util/types'),
'internal/util/inspect': X['internal/util/inspect'],
'internal/validators': X['internal/validators'],
Expand Down
Loading
Loading