Skip to content

fix(cjs): make module/exports reassignable so var module = X works (#3527) - #3764

Merged
proggeramlug merged 4 commits into
mainfrom
fix/3527-cjs-module-reassignable
May 31, 2026
Merged

proggeramlug merged 4 commits into
mainfrom
fix/3527-cjs-module-reassignable

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

A CJS module that rebinds module/exports — var module = X, module = X, or exports = X — silently failed under Perry's CJS→ESM wrapper, which declared them as const. The reassignment collided with the const and was dropped, so reads still saw the wrapper's original object. This fixes that, advancing the #3527 Express compile past iconv-lite.

Root cause

The wrapper template was:

const _cjs = (function() {
  const module = { exports: {} };
  const exports = module.exports;
  ...body...
  return module.exports;
})();

iconv-lite's encodings/index.js does:

for (var i = 0; i < modules.length; i++) {
  var module = modules[i]            // ← collides with `const module`, dropped
  mergeModules(exports, module)      // reads module.<key>
}

module stayed the wrapper object (no cesu8 key), so module["cesu8"] read undefined → TypeError: Cannot read properties of undefined (reading 'cesu8') during init. (Minimal repro: var module = src; module.cesu8 → undefined.)

Fix

In Node, module/exports are wrapper-function parameters — freely reassignable, and rebinding the local is harmless because Node reads the real exports from its own module reference. Mirror that:

const __cjs_module = { exports: {} };   // stable ref, read back at the end
var module = __cjs_module;              // reassignable
var exports = __cjs_module.exports;     // reassignable
...body...
return __cjs_module.exports;

A body reassigning its module local can no longer clobber what the module exports, and var module = X binds as expected.

Verification

  • Minimal var module = X; module.cesu8 repro and the iconv-lite shape now correct; the Express tree advances past iconv-lite.
  • No regression: module.exports = X, exports.foo = …, module.exports.bar = … all unchanged (the return __cjs_module.exports reads the stable ref).
  • New unit test asserts the reassignable-var + stable-ref wrapper shape; all 53 cjs_wrap tests pass. cargo fmt --check clean.

Part of the #3527 chain (prior: #3537/#3543/#3546 codegen, #3551 wildcard, #3733 SIGBUS, #3751 Object.hasOwn). No version bump / changelog — folded in at merge per the worktree-PR convention.

…3527)

The CJS→ESM wrapper declared `const module = { exports: {} }` and
`const exports = module.exports`. A CJS body that rebinds either —
`var module = X`, `module = X`, or `exports = X` — silently failed: the
reassignment collided with the `const` and was dropped, so reads still saw the
wrapper's object. iconv-lite's `encodings/index.js` does exactly this:

    for (var i = 0; i < modules.length; i++) {
      var module = modules[i]
      mergeModules(exports, module)   // reads module.<key> → undefined
    }

`module` stayed the wrapper object (no `cesu8`), so `module[key]` read
`undefined`, throwing `Cannot read properties of undefined (reading 'cesu8')`
during init — a hard blocker compiling Express (#3527).

In Node, `module`/`exports` are wrapper-function *parameters*, freely
reassignable; rebinding the local is harmless because Node reads the real
exports from its own module reference. Mirror that: introduce a stable
`__cjs_module` (read back by `return __cjs_module.exports`) and make `module`
and `exports` reassignable `var`s aliasing it. A body reassigning its `module`
local can no longer clobber what the module exports, and `var module = X` now
binds as expected.

Verified: the iconv-shape repro and the Express tree advance past it; normal
`module.exports = X`, `exports.foo = …`, and `module.exports.bar = …` are
unchanged. Unit test asserts the reassignable-var + stable-ref wrapper shape.
@proggeramlug
proggeramlug merged commit 233aa0e into main May 31, 2026
10 of 11 checks passed
@proggeramlug
proggeramlug deleted the fix/3527-cjs-module-reassignable branch May 31, 2026 17:15
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.

1 participant