Skip to content

fix(ext/node): make process.constructor a proper instanceof check#33447

Merged
nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter24
Apr 25, 2026
Merged

fix(ext/node): make process.constructor a proper instanceof check#33447
nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter24

Conversation

@nathanwhitbot

Copy link
Copy Markdown
Contributor

Summary

Node creates process from a v8 FunctionTemplate named "process", so process instanceof process.constructor is true and the prototype's constructor property is non-enumerable (see CreateProcessObject in src/node_process_object.cc).

Deno's polyfill set Process.prototype.constructor to a separate no-op function _process (only used to surface "process" in error messages), and the assignment came through as a writable, enumerable data property. As a result:

  • process instanceof process.constructor was false (process is an instance of Process, not _process).
  • Object.getOwnPropertyDescriptor(proto, 'constructor').enumerable was true.

Drop the dummy and point Process.prototype.constructor at Process itself with the right descriptor (writable, non-enumerable, configurable). Rename the constructor's .name to "process" so error messages like Cannot delete property 'exitCode' of #<process> still read the same.

Enables parallel/test-process-prototype.js in the node compat suite.

Test plan

  • cargo test --test node_compat -- test-process-prototype passes
  • Sibling configured process tests still pass (test-process-default, test-process-env-delete, test-process-env-deprecation, test-process-emitwarning, test-process-cpuUsage, test-process-features)
  • Smoke check: process.constructor.name === 'process', process instanceof process.constructor is true, prototype-constructor descriptor is { writable: true, enumerable: false, configurable: true }

Node creates `process` from a v8 FunctionTemplate named "process",
so `process instanceof process.constructor` is true and the
prototype's `constructor` property is non-enumerable (see
CreateProcessObject in src/node_process_object.cc).

Deno's polyfill set `Process.prototype.constructor` to a separate
no-op function `_process` (only used to surface "process" in error
messages), and the assignment came through as a writable, enumerable
data property. As a result:

- `process instanceof process.constructor` was false (process is an
  instance of `Process`, not `_process`).
- `Object.getOwnPropertyDescriptor(proto, 'constructor').enumerable`
  was true.

Drop the dummy and point `Process.prototype.constructor` at `Process`
itself with the right descriptor (writable, non-enumerable,
configurable). Rename the constructor function to "process" so error
messages still read "#<process>" as before.

Enables parallel/test-process-prototype.js in the node compat suite.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Verified against Node source and the enabled parallel/test-process-prototype.js.

Correctness — all 6 test assertions covered

Test at parallel/test-process-prototype.js:1-16:

assert(process instanceof process.constructor);
assert(proto instanceof EventEmitter);
const desc = Object.getOwnPropertyDescriptor(proto, 'constructor');
assert.strictEqual(desc.value, process.constructor);
assert.strictEqual(desc.writable, true);
assert.strictEqual(desc.enumerable, false);
assert.strictEqual(desc.configurable, true);

PR effect:

Assertion Pre-PR Post-PR
process instanceof process.constructor process.constructor === _process (dummy), and process was created from Process → false ❌ process.constructor === Process, process is new Process() → true ✅
proto instanceof EventEmitter Process.prototype = Object.create(EventEmitter.prototype) — already true ✅ unchanged ✅
desc.value === process.constructor desc.value === _process, process.constructor === _process (matches but wrong identity) desc.value === Process, process.constructor === Process
desc.writable === true The pre-PR Process.prototype.constructor = _process assignment uses [[Set]] semantics on a constructor-descriptor that's writable: true (default) — so true. Pass. PR explicit writable: true
desc.enumerable === false Plain assignment leaves the descriptor enumerable: true (the inherited descriptor on Object.prototype is non-enumerable but assignment creates a new own property with enumerable: true) — fail ❌ PR explicit enumerable: false
desc.configurable === true configurable: true via assignment. Pass. PR explicit configurable: true

So pre-PR fails on assertions 1, 5, and (depending on JS engine semantics) 4. PR fixes all.

Process.name = "process" rename

V8 surfaces #<Process> in error messages like Cannot delete property 'exitCode' of #<process> based on the constructor's .name. Pre-PR, the dummy function process() {} had .name === 'process', preserving the error format. Post-PR, the real Process constructor's name is overridden via Object.defineProperty(Process, "name", { value: "process", configurable: true }) — same result. ✅

The descriptor uses __proto__: null to avoid prototype-pollution influence on the descriptor object itself — clean defensive style consistent with elsewhere in the file.

The descriptor-only-with-value-and-configurable form means writable: false, enumerable: false are inherited from the existing .name descriptor (per Object.defineProperty "merge" semantics on existing properties). End state: {value: "process", writable: false, enumerable: false, configurable: true} — exactly Node's.

__proto__: null consistency

Both defineProperty calls use __proto__: null on the descriptor object. Defensive against Object.prototype.foo = ... polluting the descriptor's prototype chain. Good style.

Dummy _process removal

Confirmed via grep -n "_process\b" on the PR branch — only the path import ext:deno_node/_process/process.ts and _exiting from _process/exiting.ts remain (different concept entirely, the _process directory). The standalone _process = function process() {} is the only deletion.

Existing in-tree test compatibility

tests/unit_node/process_test.ts:1268-1274 tests process.constructor.call({}) doesn't throw (regression for issue 23863). Trace post-PR:

  • process.constructor === Process
  • Process.call({})this is {}, this instanceof Process is false → returns new Process()
  • new Process()this is fresh Process, EventEmitter.call(this) initializes it → returns the instance

No throw. ✅ Behavior change is "returns a new Process instance" vs. the pre-PR no-op return; the test only asserts no-throw, so it's covered.

Minor (non-blocking)

The PR description correctly mirrors Node's CreateProcessObject behavior — process originating from a FunctionTemplate named "process". Worth noting that the V8 binding also has process not exposed as a global constructor (you can't new process.constructor() outside Node's bootstrapping); Deno's process.constructor.call({}) test demonstrates this is observable. Pre-PR Deno mismatched in what process.constructor is; post-PR it's Process, callable.

Safety / performance

  • Two Object.defineProperty calls at module load time. One-shot; no runtime overhead.
  • No new allocations on the happy path.
  • Object.defineProperty is from the global, but this is bootstrap code where primordials aren't yet a concern (it runs once at module init before user code).

LGTM.

V8's `Cannot delete property 'X' of #<...>` error message uses the
constructor's syntactic name captured at parse time, not the runtime
`.name` set via `Object.defineProperty`. test-process-exit-code-validation
asserts `#<process>` (lowercase) but Deno was emitting `#<Process>`
(capital).

Switch to a named function expression `const Process = function process(...)`
so the syntactic name is 'process'. Drop the now-redundant
`Object.defineProperty(Process, 'name', …)` since the name is correct
at parse time.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

Named function expression const Process = function process(...) correctly bakes the lowercase name into V8's parse-time identity so #<process> shows up in error messages — defineProperty(F, 'name', …) wouldn't. prototype.constructor descriptor (writable: true, enumerable: false, configurable: true) matches Node's FunctionTemplate-based binding.

Extension code must be 7-bit ASCII; the snapshot build crashed with
'Extension code must be 7-bit ASCII: node:process (found …)'.
@nathanwhit
nathanwhit merged commit dc88ad5 into denoland:main Apr 25, 2026
112 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants