fix(ext/node): make process.constructor a proper instanceof check#33447
Conversation
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
left a comment
There was a problem hiding this comment.
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 === ProcessProcess.call({})→thisis{},this instanceof Processis false → returnsnew Process()new Process()→thisis 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.definePropertycalls at module load time. One-shot; no runtime overhead. - No new allocations on the happy path.
Object.definePropertyis 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.
|
Named function expression |
Extension code must be 7-bit ASCII; the snapshot build crashed with 'Extension code must be 7-bit ASCII: node:process (found …)'.
Summary
Node creates
processfrom a v8 FunctionTemplate named"process", soprocess instanceof process.constructoris true and the prototype'sconstructorproperty is non-enumerable (seeCreateProcessObjectinsrc/node_process_object.cc).Deno's polyfill set
Process.prototype.constructorto 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.constructorwas false (processis an instance ofProcess, not_process).Object.getOwnPropertyDescriptor(proto, 'constructor').enumerablewas true.Drop the dummy and point
Process.prototype.constructoratProcessitself with the right descriptor (writable, non-enumerable, configurable). Rename the constructor's.nameto"process"so error messages likeCannot delete property 'exitCode' of #<process>still read the same.Enables
parallel/test-process-prototype.jsin the node compat suite.Test plan
cargo test --test node_compat -- test-process-prototypepassestest-process-default,test-process-env-delete,test-process-env-deprecation,test-process-emitwarning,test-process-cpuUsage,test-process-features)process.constructor.name === 'process',process instanceof process.constructoristrue, prototype-constructor descriptor is{ writable: true, enumerable: false, configurable: true }