Summary
The bare core specifier 'events' (no node: prefix) resolves to a broken EventEmitter whose instances have an empty prototype — setMaxListeners, on, once, emit, removeListener, etc. are all undefined. The node:-prefixed form 'node:events' resolves to the correct, full EventEmitter. 'events' and 'node:events' must be the same builtin.
This is the next ink wall (the #4993 process fix let init proceed into signal-exit, which does require('events')).
Evidence (same binary, post-#4993)
import { EventEmitter } from 'node:events':
new EventEmitter().setMaxListeners = function ✓ (.on/.emit/.once/.removeListener all function)
setMaxListeners(Infinity) → OK
import EE from 'events': // bare specifier
typeof EE = function
new EE().setMaxListeners = undefined ✗
new EE().on = undefined ✗
→ TypeError: setMaxListeners is not a function
import * as ev from 'events':
ev.EventEmitter = function, ev.default = function
new ev.EventEmitter().setMaxListeners = undefined ✗
So bare 'events' yields a constructor (typeof function) but its new instances carry no prototype methods, whereas 'node:events' yields the complete class. Same for require('events') (the CJS form most older packages use).
Minimal repro
import EE from 'events';
console.log(typeof new EE().setMaxListeners); // Perry: undefined ✗
import { EventEmitter } from 'node:events';
console.log(typeof new EventEmitter().setMaxListeners); // Perry: function ✓
Node: both function.
How it surfaced (ink, #348)
signal-exit/index.js (transitive via cli-cursor → restore-cursor) runs at init:
var EE = require('events');
if (typeof EE !== 'function') EE = EE.EventEmitter;
emitter = process.__signal_exit_emitter__ = new EE();
...
emitter.setMaxListeners(Infinity); // → TypeError: setMaxListeners is not a function
Because require('events') returns the stub EventEmitter, new EE() has no setMaxListeners, throwing during module init — before any user code.
Scope / suggested fix
Impact
Related
Summary
The bare core specifier
'events'(nonode:prefix) resolves to a brokenEventEmitterwhose instances have an empty prototype —setMaxListeners,on,once,emit,removeListener, etc. are allundefined. Thenode:-prefixed form'node:events'resolves to the correct, fullEventEmitter.'events'and'node:events'must be the same builtin.This is the next ink wall (the #4993 process fix let init proceed into
signal-exit, which doesrequire('events')).Evidence (same binary, post-#4993)
So bare
'events'yields a constructor (typeof function) but itsnewinstances carry no prototype methods, whereas'node:events'yields the complete class. Same forrequire('events')(the CJS form most older packages use).Minimal repro
Node: both
function.How it surfaced (ink, #348)
signal-exit/index.js(transitive viacli-cursor→restore-cursor) runs at init:Because
require('events')returns the stub EventEmitter,new EE()has nosetMaxListeners, throwing during module init — before any user code.Scope / suggested fix
node:-prefixed form.'events'must be identical to'node:events'. The bare form currently yields a constructor whose prototype is empty (no methods installed)..env/.stdout/.platformundefined (only bareprocessis wired); blocks terminal-size → ink #4987 was the analogous bare-vs-node:split forprocess. Worth auditing other core modules (stream,util,buffer,path,os, …) for the same bare-specifier divergence — manycompilePackagesdeps import the bare form.Impact
require('events')/import … from 'events'(bare) and instantiating an EventEmitter — i.e. a very large fraction of the npm ecosystem (signal-exit, and countless others).ink(React-based TUI framework) end-to-end viaperry.compilePackages#348): blocks init via signal-exit. After it, ink's next gate is yoga-layout's WASM runtime (out-of-scope).Related
ink(React-based TUI framework) end-to-end viaperry.compilePackages#348 (ink end-to-end), node:process: imported default export & globalThis.process are an incomplete stub —.env/.stdout/.platformundefined (only bareprocessis wired); blocks terminal-size → ink #4987/fix(runtime): node:process default import & globalThis.process expose the full process object (#4987) #4993 (bare-vs-node:processsplit — same family)export default class Name { … }(ESM) drops the prototype method table — onlyconstructorsurvives (ESM sibling of #4933; blocks ink render) #4976/fix(hir): lower inlineexport default classbodies — methods/fields survive (#4976) #4981 (class method-table — instances-without-methods symptom is similar but unrelated cause)