Skip to content

fix(compile): resolve barrel/type-only default-import link wall — nestjs reaches runtime (#4872)#4885

Merged
proggeramlug merged 1 commit into
mainfrom
fix-4872-barrel-default-link-wall
Jun 10, 2026
Merged

fix(compile): resolve barrel/type-only default-import link wall — nestjs reaches runtime (#4872)#4885
proggeramlug merged 1 commit into
mainfrom
fix-4872-barrel-default-link-wall

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes the native-link wall from #4872: undefined …__default / __perry_wrap_perry_fn_…__default symbols for import barrels and type-only re-export surfaces. The nestjs-hello fixture now compiles and links end-to-end; it proceeds to a new (different) runtime wall documented in its WALLS.md.

Root causes & fixes

The CJS wrap lowers every require('X') to import _req_N from 'X' (a default import). Four gaps compounded behind that:

  1. Default import of a module with no default export (crates/perry/src/commands/compile.rs) — rxjs resolves to its TS source src/index.ts, a barrel with only named exports; uid ships dist/index.mjs, same shape. The classifier unconditionally registered the local as a callable function import, so value reads emitted __perry_wrap_perry_fn_<src>__default and direct reads emitted perry_fn_<src>__default — symbols nothing defines. Now, when the resolved origin module has no default in its (propagated) export map, the local routes through the existing namespace-import machinery — Node's require(esm) semantics: member calls (rxjs_1.lastValueFrom(...)) resolve per-export to origin symbols, and whole-value reads materialize the namespace object.

  2. __exportStar(require("./x"), exports) barrels (cjs_wrap/wrap.rs, extract_requires.rs) — tsc's CJS lowering of export * from './x' was invisible to the wrap, so multi-level barrels (@nestjs/common/index.jsdecorators/index.jscore/index.jscontroller.decorator.js) surfaced no static re-exports and import { Controller } from '@nestjs/common' link-failed on perry_fn_<common_index_js>__Controller. The wrap now also emits a real export * from '<spec>' for each __exportStar call (bare, tslib_1.-member, and (0, …)-sequenced forms), letting the existing transitive re-export propagation resolve names to their defining module. Specs covered by the star re-export are excluded from the recursive named-export pull so the static origin binding isn't shadowed by an export const X = _cjs.X runtime read.

  3. Type-only interface modules (cjs_wrap/detect.rs) — nestjs dist *.interface.js files contain only Object.defineProperty(exports, "__esModule", { value: true }); — no exports.X =, no require( — so is_commonjs missed them and they compiled as zero-export ES modules, throwing ReferenceError: exports is not defined at init once the link succeeded. defineProperty(exports, is now a CJS marker.

  4. export * leaked default across hops (compile.rs propagation loop) — spec-incorrect, and it would poison the has-default probe in (1) for barrels whose star sources have defaults.

Drive-by: perry-hir/lower_decl/class_validation.rs counted TS constructor overload signatures (body-less constructor(…);) as real constructors, rejecting rxjs's Notification (3 signatures + 1 implementation) with SyntaxError: class may only have one constructor. Only constructors with bodies count now.

Fixture status

tests/release/packages/nestjs-hello now reaches Wrote executable (~41 MB). It still SKIPs (WALLS.md retained): the boot dies in @nestjs/common/services/logger.service.js init because .prototype of a capturing class expression (ClassExprFresh lowering — trigger is a getter capturing an outer variable) reads undefined, so tslib's __decorate throws Cannot convert undefined or null to object. WALLS.md Wall 2 has a 10-line minimal repro for that follow-up.

Validation

  • New e2e test crates/perry/tests/issue_4872_barrel_default_reexports.rs covers all three shapes (type-only surface, 2-level __exportStar chain, no-default ESM member call) — compile, link, run, byte-exact output.
  • Full cargo test --release -p perry green, including the Stripe SDK: stripe.products.create throws 'replace is not a function' (request dispatch, flat body) after #4831 #4841 namespace-CJS classifier regression test (closest neighbor to the changed code).
  • Release fixtures: hono-basic PASS, node-http-serve PASS. axios-get / ws-echo fail identically with the pre-change baseline binary (pre-existing, unrelated).
  • cargo fmt --all -- --check and scripts/check_file_size.sh clean.

No version bump / changelog per contributor convention — maintainer folds in at merge.

Refs #4872 (link wall — resolved here; runtime wall remains tracked in the fixture's WALLS.md).

…js) (#4872)

Four coordinated fixes for the nestjs-hello native-link wall:

1. compile.rs: a DEFAULT import of a compiled module that has no
   `default` export (ESM barrel with only named exports, or a type-only
   interface surface with no exports at all) now routes through the
   namespace-import machinery instead of registering a phantom callable.
   Member reads resolve per-export to origin symbols and whole-value
   reads materialize the namespace object — Node `require(esm)`
   semantics. Pre-fix the consumer emitted
   `perry_fn_<src>__default` / `__perry_wrap_perry_fn_<src>__default`
   references that no object file defines.

2. cjs_wrap: `__exportStar(require("./x"), exports)` — tsc's CJS
   lowering of `export * from "./x"` — now also emits the real ESM
   `export * from './x'` at module scope, so compile.rs's transitive
   re-export propagation resolves named imports through multi-level
   barrels to the defining module (`@nestjs/common` → `decorators` →
   `core` → `controller.decorator.js`). The specs covered by the star
   re-export are skipped by the recursive named-export pull so the
   static origin binding isn't shadowed by an `export const X = _cjs.X`
   runtime read.

3. cjs_wrap detect: modules whose only CJS marker is
   `Object.defineProperty(exports, "__esModule", { value: true });`
   (nestjs dist `*.interface.js`) are now detected as CommonJS. Pre-fix
   they compiled as zero-export ES modules and threw
   `ReferenceError: exports is not defined` at init.

4. compile.rs: `export *` propagation no longer re-exports `default`
   across hops (ESM spec), which keeps the has-default probe in (1)
   sound for barrels whose star sources have default exports.

Drive-by: class_validation.rs counted TypeScript constructor overload
SIGNATURES (body-less `constructor(...)` declarations) as real
constructors, rejecting rxjs's `Notification` (3 signatures + 1
implementation) with "may only have one constructor". Only the
implementation counts now.

The nestjs-hello fixture now compiles and links end-to-end
(`Wrote executable`, ~41 MB). It still SKIPs at runtime on the next
wall — `.prototype` of a capturing class expression (`ClassExprFresh`)
reads undefined, so tslib's `__decorate` throws during
`@nestjs/common/services/logger.service.js` init. WALLS.md documents
the minimal repro for that follow-up.

Validation: new e2e test issue_4872_barrel_default_reexports.rs covers
all three shapes; full `cargo test -p perry` green (incl. the #4841
namespace-CJS classifier test); hono-basic + node-http-serve release
fixtures PASS; axios-get / ws-echo failures reproduce identically with
the pre-change baseline binary (pre-existing).
@proggeramlug proggeramlug merged commit 0c37163 into main Jun 10, 2026
13 checks passed
@proggeramlug proggeramlug deleted the fix-4872-barrel-default-link-wall branch June 10, 2026 07:52
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