You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a compilePackages (CJS) module, a top-level var initialized by require() and later reassigned is mis-compiled: the variable is correct immediately after the require, but any statement that reassigns it reads the binding as not-defined / undefined, corrupting it for the rest of the module. Reassignment is the trigger; closure capture is not involved.
This is the next ink wall (the #4993/#5002 process+events fixes let init proceed into signal-exit).
Minimal repro
node_modules/pkg/data.js:
module.exports=['a','b','c'];
node_modules/pkg/index.js:
vars=require('./data.js');// here: typeof s === 'object', s is ['a','b','c'] ✓s=s.filter(function(){returntrue;});// ← TypeError/ReferenceError: s is not definedmodule.exports=s;
importxfrom'pkg';// (pkg in compilePackages + allow.compilePackages)
→ ReferenceError: s is not defined (or s reads undefined), even though a probe right after the require shows s is the array.
Bisection (each in its own binary)
var s = require(...); var f = () => s; // capture only → s=object, works ✓
var s = require(...); s = (s||[]).filter(() => true); // reassign only → s=object then "s is not defined" ✗
var s = require(...); var f = () => s; s = s.filter(); // both → ✗
Only the reassignment variants fail. A probe (typeof s) placed between the require and the reassignment prints object — so the value arrives, then the reassignment statement can't see the binding.
signal-exit/index.js (transitive via cli-cursor → restore-cursor):
varsignals=require('./signals.js')// ['SIGABRT', ...] — fine immediately after...signals.forEach(function(sig){ ... })// line 110, top-level → TypeError: undefined.forEach...signals=signals.filter(function(sig){ ... })// line 156 — the reassignment that corrupts `signals`
An in-module probe confirms require('./signals.js') returns the array (typeof === 'object') but signals reads undefined at the forEach, because the module also reassigns signals later. require('assert') in the same spot is unaffected (it's never reassigned). signal-exit throws at init → render() (and any ink program) dies before output.
Likely mechanism
A module-level var that is reassigned appears to be lowered to a separate (boxed / block-scoped) binding, but the require() initializer is written to the original/unboxed slot — so reads through the reassigned binding see the uninitialized value. Reads before any reassignment in source still observe undefined because the binding is hoisted module-wide. (Plain var x = require(); use(x) without reassignment works; var x = 1; x = 2 non-require reassignment presumably works too — the interaction is require()-initializer + reassignment.)
Impact
Any compilePackages CJS module that reassigns a require()-initialized top-level var — a very common pattern (x = x.filter(...), x = x || fallback, lazy re-resolve, etc.). signal-exit is the immediate one.
Summary
In a
compilePackages(CJS) module, a top-levelvarinitialized byrequire()and later reassigned is mis-compiled: the variable is correct immediately after therequire, but any statement that reassigns it reads the binding as not-defined / undefined, corrupting it for the rest of the module. Reassignment is the trigger; closure capture is not involved.This is the next ink wall (the #4993/#5002 process+events fixes let init proceed into
signal-exit).Minimal repro
node_modules/pkg/data.js:node_modules/pkg/index.js:→
ReferenceError: s is not defined(orsreadsundefined), even though a probe right after therequireshowssis the array.Bisection (each in its own binary)
Only the reassignment variants fail. A probe (
typeof s) placed between therequireand the reassignment printsobject— so the value arrives, then the reassignment statement can't see the binding.How it surfaced (ink, #348)
signal-exit/index.js(transitive viacli-cursor→restore-cursor):An in-module probe confirms
require('./signals.js')returns the array (typeof === 'object') butsignalsreadsundefinedat theforEach, because the module also reassignssignalslater.require('assert')in the same spot is unaffected (it's never reassigned). signal-exit throws at init →render()(and any ink program) dies before output.Likely mechanism
A module-level
varthat is reassigned appears to be lowered to a separate (boxed / block-scoped) binding, but therequire()initializer is written to the original/unboxed slot — so reads through the reassigned binding see the uninitialized value. Reads before any reassignment in source still observeundefinedbecause the binding is hoisted module-wide. (Plainvar x = require(); use(x)without reassignment works;var x = 1; x = 2non-require reassignment presumably works too — the interaction isrequire()-initializer + reassignment.)Impact
compilePackagesCJS module that reassigns arequire()-initialized top-levelvar— a very common pattern (x = x.filter(...),x = x || fallback, lazy re-resolve, etc.).signal-exitis the immediate one.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 + core modules: bare specifiereventsresolves to a stub EventEmitter (empty prototype, no setMaxListeners/on/emit) —node:eventsworks; blocks signal-exit → ink #4995/fix(hir,runtime,stdlib): bareeventsspecifier resolves to full EventEmitter (#4995) #5002 (the process/events surface fixes that exposed this)