Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,5 @@
- Support `ws` server-connection `pause()`, `resume()`, and `isPaused` with native socket read backpressure.

- Validate `net`, `http`, and `https` listen ports synchronously before binding, preserving asynchronous bind errors and Node-compatible numeric string parsing. Adapts string routing from [oven-sh/bun#34083](https://github.com/oven-sh/bun/pull/34083). Thanks @robobun!

- Match Node process property descriptors, including lazy `argv`/`execArgv` data properties and descriptor replacement in native argument readers. Adapts [oven-sh/bun#34229](https://github.com/oven-sh/bun/pull/34229) and [oven-sh/bun#44356](https://github.com/oven-sh/bun/pull/44356). Thanks @robobun!
2 changes: 2 additions & 0 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ Socket close notifications use internal scheduling and continue to run when user

### [`node:process`](https://nodejs.org/api/process.html)

`process.platform`, `arch`, `version`, `versions`, `pid`, and `release` expose Node-compatible data descriptors. `argv` and `execArgv` initialize lazily and support descriptor-based replacement. `ppid` and `title` retain their native behavior behind data descriptors. `exitCode` remains a non-configurable accessor.

Initializing `process.stdout` or `process.stderr` does not retain a duplicate pipe writer. Explicitly closing the underlying descriptor publishes EOF while the process remains alive. On POSIX, socket and pipe stdio use nonblocking writes. `end()` and pipeline completion flush pending output and shut down the write side of socket stdio, so peers receive EOF before process exit. Actual pipes stay open until process exit or explicit descriptor closure. `destroy()` preserves fd 1/2; after POSIX socket or pipe finalization, later stream writes fail with `EPIPE`. On Windows, piped stdout/stderr reset their writable state after `end()` or pipeline completion and accept subsequent writes.

`NODE_OPTIONS` applies Bun-supported Node runtime flags before command-line arguments, including `--require`/`-r` and `--import` preloads. Injected flags are absent from `process.execArgv`. Unsupported Node flags remain ignored; flags outside the Node allowlist warn and are ignored. `process.config.variables.node_without_node_options` remains `true` because support is partial.
Expand Down
119 changes: 27 additions & 92 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3202,105 +3202,42 @@ static JSValue constructExecPath(VM& vm, JSObject* processObject)
return JSValue::decode(Bun__Process__getExecPath(globalObject));
}

extern "C" EncodedJSValue Bun__Process__getArgv(JSGlobalObject* lexicalGlobalObject)
static JSValue constructArgv(VM& vm, JSObject* processObject)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto* process = globalObject->processObject();
if (!process) {
return JSValue::encode(jsUndefined());
}

return JSValue::encode(process->getArgv(globalObject));
return JSValue::decode(Bun__Process__createArgv(processObject->globalObject()));
}

// get from js
JSC_DEFINE_CUSTOM_GETTER(processArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
static JSValue constructExecArgv(VM& vm, JSObject* processObject)
{
Process* process = getProcessObject(globalObject, JSValue::decode(thisValue));
if (!process) {
return JSValue::encode(jsUndefined());
}

return JSValue::encode(process->getArgv(globalObject));
return JSValue::decode(Bun__Process__createExecArgv(processObject->globalObject()));
}

JSValue Process::getArgv(JSGlobalObject* globalObject)
{
if (auto argv = m_argv.get()) {
return argv;
}

JSValue argv = JSValue::decode(Bun__Process__createArgv(globalObject));
setArgv(globalObject, argv);
return argv;
return get(globalObject, Identifier::fromString(globalObject->vm(), "argv"_s));
}

void Process::setArgv(JSGlobalObject* globalObject, JSValue value)
JSValue Process::getExecArgv(JSGlobalObject* globalObject)
{
auto& vm = globalObject->vm();
m_argv.set(vm, this, value);
return get(globalObject, Identifier::fromString(globalObject->vm(), "execArgv"_s));
}

JSC_DEFINE_CUSTOM_SETTER(setProcessArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName))
extern "C" EncodedJSValue Bun__Process__getArgv(JSGlobalObject* lexicalGlobalObject)
{
Process* process = getProcessObject(globalObject, JSValue::decode(thisValue));
if (!process) {
return true;
}

JSValue value = JSValue::decode(encodedValue);
process->setArgv(globalObject, value);
return true;
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
JSValue argv = globalObject->processObject()->getArgv(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(argv);
}

extern "C" EncodedJSValue Bun__Process__getExecArgv(JSGlobalObject* lexicalGlobalObject)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto* process = globalObject->processObject();
if (!process) {
return JSValue::encode(jsUndefined());
}

return JSValue::encode(process->getExecArgv(globalObject));
}

JSC_DEFINE_CUSTOM_GETTER(processExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
{
Process* process = getProcessObject(globalObject, JSValue::decode(thisValue));
if (!process) {
return JSValue::encode(jsUndefined());
}

return JSValue::encode(process->getExecArgv(globalObject));
}

JSValue Process::getExecArgv(JSGlobalObject* globalObject)
{
if (auto argv = m_execArgv.get()) {
return argv;
}

JSValue argv = JSValue::decode(Bun__Process__createExecArgv(globalObject));
setExecArgv(globalObject, argv);
return argv;
}

void Process::setExecArgv(JSGlobalObject* globalObject, JSValue value)
{
auto& vm = globalObject->vm();
m_execArgv.set(vm, this, value);
}

JSC_DEFINE_CUSTOM_SETTER(setProcessExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName))
{
Process* process = getProcessObject(globalObject, JSValue::decode(thisValue));
if (!process) {
return true;
}

JSValue value = JSValue::decode(encodedValue);
process->setExecArgv(globalObject, value);
return true;
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
JSValue execArgv = globalObject->processObject()->getExecArgv(globalObject);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(execArgv);
}

JSC_DEFINE_CUSTOM_GETTER(processGetEval, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))
Expand Down Expand Up @@ -3808,8 +3745,6 @@ void Process::visitChildrenImpl(JSCell* cell, Visitor& visitor)
visitor.append(thisObject->m_uncaughtExceptionCaptureCallback);
visitor.append(thisObject->m_nextTickFunction);
visitor.append(thisObject->m_cachedCwd);
visitor.append(thisObject->m_argv);
visitor.append(thisObject->m_execArgv);
visitor.append(thisObject->m_onWarning);

thisObject->m_cpuUsageStructure.visit(visitor);
Expand Down Expand Up @@ -5103,8 +5038,8 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
allowedNodeEnvironmentFlags constructAllowedNodeEnvironmentFlags PropertyCallback
loadEnvFile constructLoadEnvFile PropertyCallback
finalization constructFinalization PropertyCallback
arch constructArch PropertyCallback
argv processArgv CustomAccessor
arch constructArch PropertyCallback|ReadOnly
argv constructArgv PropertyCallback
argv0 constructArgv0 PropertyCallback
assert Process_functionAssert Function 1
availableMemory Process_availableMemory Function 0
Expand All @@ -5123,7 +5058,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
dlopen Process_functionDlopen Function 1
emitWarning Process_emitWarning Function 1
env constructEnv PropertyCallback
execArgv processExecArgv CustomAccessor
execArgv constructExecArgv PropertyCallback
execPath constructExecPath PropertyCallback
execve Process_functionExecve Function 3
exit Process_functionExit Function 1
Expand All @@ -5141,12 +5076,12 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
moduleLoadList Process_stubEmptyArray PropertyCallback
nextTick constructProcessNextTickFn PropertyCallback
openStdin Process_functionOpenStdin Function 0
pid constructPid PropertyCallback
platform constructPlatform PropertyCallback
ppid processPpid CustomAccessor
pid constructPid PropertyCallback|ReadOnly
platform constructPlatform PropertyCallback|ReadOnly
ppid processPpid CustomValue
reallyExit Process_functionReallyExit Function 1
ref Process_ref Function 1
release constructProcessReleaseObject PropertyCallback
release constructProcessReleaseObject PropertyCallback|ReadOnly
report constructProcessReportObject PropertyCallback
resourceUsage Process_functionResourceUsage Function 0
revision constructRevision PropertyCallback
Expand All @@ -5156,12 +5091,12 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
stderr constructStderr PropertyCallback
stdin constructStdin PropertyCallback
stdout constructStdout PropertyCallback
title processTitle CustomAccessor
title processTitle CustomValue
umask Process_functionUmask Function 1
unref Process_unref Function 1
uptime Process_functionUptime Function 1
version constructVersion PropertyCallback
versions constructVersions PropertyCallback
version constructVersion PropertyCallback|ReadOnly
versions constructVersions PropertyCallback|ReadOnly

#if !OS(WINDOWS)
getegid Process_functiongetegid Function 0
Expand Down
5 changes: 0 additions & 5 deletions src/jsc/bindings/BunProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ class Process : public WebCore::JSEventEmitter {
WriteBarrier<JSObject> m_nextTickFunction;
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/bootstrap/switches/does_own_process_state.js#L113-L116
WriteBarrier<JSString> m_cachedCwd;
WriteBarrier<Unknown> m_argv;
WriteBarrier<Unknown> m_execArgv;
// The JS warning printer (ProcessObjectInternals createOnWarning), built on the first warning.
WriteBarrier<JSObject> m_onWarning;

Expand Down Expand Up @@ -93,10 +91,7 @@ class Process : public WebCore::JSEventEmitter {
void clearCachedCwd() { m_cachedCwd.clear(); }

JSValue getArgv(JSGlobalObject* globalObject);
void setArgv(JSGlobalObject* globalObject, JSValue argv);

JSValue getExecArgv(JSGlobalObject* globalObject);
void setExecArgv(JSGlobalObject* globalObject, JSValue execArgv);

static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject,
JSC::JSValue prototype)
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/InspectorLifecycleAgent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ Protocol::ErrorStringOr<ModuleGraph> InspectorLifecycleAgent::getModuleGraph()
Ref<JSON::ArrayOf<String>> argv = JSON::ArrayOf<String>::create();
{

auto* array = uncheckedDowncast<JSC::JSArray>(process->getArgv(global));
JSC::JSValue argvValue = process->getArgv(global);
RETURN_IF_EXCEPTION(scope, fail("Failed to get argv"_s));
for (size_t i = 0, length = array->length(); i < length; i++) {
auto* array = dynamicDowncast<JSC::JSArray>(argvValue);
for (size_t i = 0, length = array ? array->length() : 0; i < length; i++) {
auto value = array->getIndex(global, i);
RETURN_IF_EXCEPTION(scope, fail("Failed to get value at index"_s));
auto string = value.toWTFString(global);
Expand Down
125 changes: 125 additions & 0 deletions test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,131 @@ async function runInlineFixture(script, expectedStdout = null, expectedCode = 0)
return { stdout, exitCode };
}

describe("process property descriptors", () => {
it.each([
["platform", false],
["arch", false],
["version", false],
["versions", false],
["env", true],
["execPath", true],
["argv", true],
["execArgv", true],
["pid", false],
["ppid", true],
["title", true],
["release", false],
])("process.%s exposes a Node-compatible data descriptor", async (name, writable) => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const assert = require("node:assert/strict");
const name = ${JSON.stringify(name)};
const descriptor = Object.getOwnPropertyDescriptor(process, name);
assert.deepEqual(Object.keys(descriptor).sort(), ["configurable", "enumerable", "value", "writable"]);
assert.equal(descriptor.writable, ${writable});
assert.equal(descriptor.enumerable, true);
assert.equal(descriptor.configurable, true);
assert.equal(descriptor.value, process[name]);
const replacement = { fixture: true };
Object.defineProperty(process, name, { ...descriptor, value: replacement });
assert.equal(process[name], replacement);
Object.defineProperty(process, name, descriptor);
assert.deepEqual(Object.getOwnPropertyDescriptor(process, name), descriptor);
if (!descriptor.writable) {
const assignStrict = Function("name", "replacement", '"use strict"; process[name] = replacement;');
assert.throws(() => assignStrict(name, replacement), TypeError);
Function("replacement", "process[" + JSON.stringify(name) + "] = replacement")(replacement);
assert.equal(process[name], descriptor.value);
}
delete process[name];
assert.equal(Object.hasOwn(process, name), false);
Object.defineProperty(process, name, descriptor);
console.log("ok");
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 });
});

it("keeps process.exitCode as a non-configurable accessor", () => {
const descriptor = Object.getOwnPropertyDescriptor(process, "exitCode");
expect(descriptor).toEqual({
get: expect.any(Function),
set: expect.any(Function),
enumerable: true,
configurable: false,
});
});

it("keeps native setters behind data descriptors until redefined", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const assert = require("node:assert/strict");
for (const name of ["argv", "execArgv", "ppid"]) {
const value = name === "ppid" ? 123 : ["fixture"];
process[name] = value;
assert.equal(process[name], value);
assert.equal(Object.getOwnPropertyDescriptor(process, name).value, value);
}
process.title = "fixture";
assert.equal(process.title, "fixture");
assert.equal(Object.getOwnPropertyDescriptor(process, "title").value, "fixture");
Object.defineProperty(process, "title", { value: "redefined" });
assert.equal(process.title, "redefined");
console.log("ok");
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 });
});
});

it.concurrent("process.argv redefinition reaches native consumers", async () => {
// parseArgs uses a different argv offset for -e; exercise a script's arguments.
using dir = tempDir("process-argv-redefinition", {
"index.cjs": `
const assert = require("node:assert/strict");
const { parseArgs } = require("node:util");
const options = { before: { type: "boolean" }, after: { type: "boolean" } };
process.argv = ["bun", "script.js", "--before"];
assert.equal(parseArgs({ options }).values.before, true);
Object.defineProperty(process, "argv", { value: ["bun", "script.js", "--after"] });
assert.equal(parseArgs({ options }).values.after, true);
const marker = new Error("argv getter");
Object.defineProperty(process, "argv", { configurable: true, get() { throw marker; } });
assert.throws(() => parseArgs({ options }), e => e === marker);
for (let i = 0; i < 2; i++) assert.throws(() => Bun.argv, e => e === marker);
const value = ["bun", "script.js", "--before"];
Object.defineProperty(process, "argv", { value });
assert.equal(Bun.argv, value);
assert.equal(parseArgs({ options }).values.before, true);
console.log("ok");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), join(String(dir), "index.cjs")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 });
});

it("process", () => {
// this property isn't implemented yet but it should at least return a string
const isNode = !process.isBun;
Expand Down
Loading