Skip to content

compilePackages: reassigning a require()-initialized top-level var corrupts the binding (reads undefined / 'not defined') — breaks signal-exit → ink #5006

@proggeramlug

Description

@proggeramlug

Summary

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:

var s = require('./data.js');
// here: typeof s === 'object', s is ['a','b','c']   ✓
s = s.filter(function () { return true; });   // ← TypeError/ReferenceError: s is not defined
module.exports = s;
import x from '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.

How it surfaced (ink, #348)

signal-exit/index.js (transitive via cli-cursorrestore-cursor):

var signals = 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.
  • ink end-to-end (Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348): blocks init via signal-exit. After it, ink's next gate is yoga-layout's WASM runtime (out-of-scope).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions