Skip to content

fix(runtime): #5437 — CJS-interop probe on module-default wrapper must not auto-call it (fixes #5546 boot crash) - #5553

Merged
proggeramlug merged 2 commits into
mainfrom
fix/w6-debug-interop-noautocall-5437
Jun 22, 2026
Merged

fix(runtime): #5437 — CJS-interop probe on module-default wrapper must not auto-call it (fixes #5546 boot crash)#5553
proggeramlug merged 2 commits into
mainfrom
fix/w6-debug-interop-noautocall-5437

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Why

Follow-up to #5546 (merged). That change auto-calls a registered CJS module-default wrapper closure on a property miss to obtain module.exports. For a CJS module whose module.exports is a function (e.g. debugcreateDebug), the wrapper is that function, and the ubiquitous _interop_require_default(require("debug")) reads .__esModule off it → miss → the fallback calls createDebug() with no argsenabled(undefined)undefined.length → throws at module-init. The server exits at boot (regressed the Next.js #5437 bundle from HTTP 500-serving to HTTP 000-boot-crash, and can break other SWC/Babel-compiled-CJS programs).

This also means #5546's reported "undefined-ctor 1→0" was a measurement artifact — the server crashed at boot before the request render where SharedCacheControls is evaluated. (Completing the actual W6 close — making the auto-call fire for the captured uw — is tracked separately and in progress.)

Fix (runtime-only, +87/-3)

crates/perry-runtime/src/object/field_get_set.rs: on a registered module-default wrapper, short-circuit the two CJS-interop probe keys before the auto-call:

  • .__esModuleundefined (a function-export CJS module is not an ES module)
  • .default → the wrapper closure itself (interop-default of a non-ESM module is module.exports)

Both added to the auto-call exclusion list. Genuine non-default member reads (the #5437 SharedCacheControls path) are unaffected.

Validation

  • Regression test module_default_wrapper_interop_probe_does_not_call_wrapper — wrapper panic!s if invoked; asserts the probe keys resolve without calling it.
  • Bundle: the .length boot crash is gone — server boots and serves again (back to HTTP 500; the remaining 1× SharedCacheControls throw is the in-progress real W6 close).
  • cargo test -p perry-runtime: 1071 pass incl. the new test.

Refs #5437, #5546.

Summary by CodeRabbit

Bug Fixes

  • Fixed a runtime crash during application boot that occurred when the CommonJS module interoperability system probed certain module wrappers. The issue was caused by unintended function invocations during module initialization, which could trigger side effects and fail at startup.

Ralph Küpper added 2 commits June 22, 2026 17:22
…ault wrapper must not auto-call it

The W6 wrapper-resolve fallback in js_object_get_field_by_name auto-calls a
registered module-default wrapper closure on a property miss to obtain
module.exports. For a CJS module whose module.exports IS a plain function
(next/dist/compiled/debug -> createDebug), the registered wrapper is that
function itself. _interop_require_default(require('debug')) reads .__esModule
off it: the auto-call ran createDebug() with no args -> enabled(undefined) ->
undefined.length -> 'Cannot read properties of undefined (reading length)',
crashing the Next.js server at boot.

Short-circuit the two CJS-interop probe keys on a registered wrapper:
- .__esModule -> undefined (a function-export CJS module is not an ES module)
- .default    -> the wrapper closure itself (interop default of a non-ESM
                 module is module.exports)
Both also excluded from the auto-call exclusion list. The genuine W6
SharedCacheControls path (non-default member reads) is unaffected.

Bundle: .length-on-undefined throw gone; Next.js server now BINDS (was
HTTP 000 boot-exit) and serves; next wall = request-time uS_constructor
new uw.SharedCacheControls captured mis-box (separate, pre-documented).

Regression test: module_default_wrapper_interop_probe_does_not_call_wrapper.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a1631d5-d8a0-42a2-834a-6b11e3646af7

📥 Commits

Reviewing files that changed from the base of the PR and between b86091d and faa1528.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • crates/perry-runtime/src/closure/tests.rs
  • crates/perry-runtime/src/object/field_get_set.rs

📝 Walkthrough

Walkthrough

Version bumped to 0.5.1201. The fix adds a short-circuit in js_object_get_field_by_name so that when a closure receiver is a registered module-default wrapper and the requested property is __esModule or default, the read returns immediately without auto-calling the wrapper. The #5437 exclusion list is also extended with these two keys. A regression test verifies the wrapper is never invoked during those probe reads.

Changes

CJS interop probe short-circuit for module-default wrappers

Layer / File(s) Summary
Short-circuit and exclusion-list fix
crates/perry-runtime/src/object/field_get_set.rs
Adds a new early-return block for module-default wrapper closures: __esModule reads return undefined and default reads return the wrapper value itself. Extends the #5437 auto-call exclusion list to cover both default and __esModule.
Regression test: wrapper not called during probe
crates/perry-runtime/src/closure/tests.rs
Adds a panic-based extern probe function and a test that registers a module-default wrapper and asserts neither __esModule nor default reads invoke the wrapper.
Version bump and changelog
Cargo.toml, CLAUDE.md, CHANGELOG.md
Workspace version incremented to 0.5.1201; changelog entry documents the CJS interop probe fix and its regression test.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PerryTS/perry#5546: Introduced the module-default wrapper auto-call logic in field_get_set.rs that this PR partially overrides with the new short-circuit for __esModule/default probe reads.

Poem

🐇 A wrapper once called without warning or care,
Made modules explode with side-effects rare.
Now __esModule and default just peek,
The wrapper stays silent — no crash, no squeak!
Short-circuit complete, the boot hops with ease. 🌿

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/w6-debug-interop-noautocall-5437

Comment @coderabbitai help to get the list of available commands and usage tips.

@proggeramlug
proggeramlug merged commit 981b324 into main Jun 22, 2026
14 of 15 checks passed
@proggeramlug
proggeramlug deleted the fix/w6-debug-interop-noautocall-5437 branch June 22, 2026 15:25
proggeramlug added a commit that referenced this pull request Jun 23, 2026
…ery (wrong layer, can't close W6) (#5558)

The captured uw reads a mis-boxed closure at request time, but is a correct
module.exports object at every registration/auto-call site (require + capture
store). So registering wrappers is a guaranteed no-op for W6, and the auto-call
introduced a CJS-function-export boot-crash regression. The real root is the
class-capture pointer mis-box; tracked under #5437.

Co-authored-by: Ralph Küpper <ralph2@skelpo.com>
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