fix(compile): resolve barrel/type-only default-import link wall — nestjs reaches runtime (#4872)#4885
Merged
Merged
Conversation
…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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the native-link wall from #4872: undefined
…__default/__perry_wrap_perry_fn_…__defaultsymbols for import barrels and type-only re-export surfaces. Thenestjs-hellofixture now compiles and links end-to-end; it proceeds to a new (different) runtime wall documented in itsWALLS.md.Root causes & fixes
The CJS wrap lowers every
require('X')toimport _req_N from 'X'(a default import). Four gaps compounded behind that:Default import of a module with no
defaultexport (crates/perry/src/commands/compile.rs) — rxjs resolves to its TS sourcesrc/index.ts, a barrel with only named exports;uidshipsdist/index.mjs, same shape. The classifier unconditionally registered the local as a callable function import, so value reads emitted__perry_wrap_perry_fn_<src>__defaultand direct reads emittedperry_fn_<src>__default— symbols nothing defines. Now, when the resolved origin module has nodefaultin its (propagated) export map, the local routes through the existing namespace-import machinery — Node'srequire(esm)semantics: member calls (rxjs_1.lastValueFrom(...)) resolve per-export to origin symbols, and whole-value reads materialize the namespace object.__exportStar(require("./x"), exports)barrels (cjs_wrap/wrap.rs,extract_requires.rs) — tsc's CJS lowering ofexport * from './x'was invisible to the wrap, so multi-level barrels (@nestjs/common/index.js→decorators/index.js→core/index.js→controller.decorator.js) surfaced no static re-exports andimport { Controller } from '@nestjs/common'link-failed onperry_fn_<common_index_js>__Controller. The wrap now also emits a realexport * from '<spec>'for each__exportStarcall (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 anexport const X = _cjs.Xruntime read.Type-only interface modules (
cjs_wrap/detect.rs) — nestjs dist*.interface.jsfiles contain onlyObject.defineProperty(exports, "__esModule", { value: true });— noexports.X =, norequire(— sois_commonjsmissed them and they compiled as zero-export ES modules, throwingReferenceError: exports is not definedat init once the link succeeded.defineProperty(exports,is now a CJS marker.export *leakeddefaultacross hops (compile.rspropagation 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.rscounted TS constructor overload signatures (body-lessconstructor(…);) as real constructors, rejecting rxjs'sNotification(3 signatures + 1 implementation) withSyntaxError: class may only have one constructor. Only constructors with bodies count now.Fixture status
tests/release/packages/nestjs-hellonow reachesWrote executable(~41 MB). It still SKIPs (WALLS.md retained): the boot dies in@nestjs/common/services/logger.service.jsinit because.prototypeof a capturing class expression (ClassExprFreshlowering — trigger is a getter capturing an outer variable) readsundefined, so tslib's__decoratethrowsCannot convert undefined or null to object. WALLS.md Wall 2 has a 10-line minimal repro for that follow-up.Validation
crates/perry/tests/issue_4872_barrel_default_reexports.rscovers all three shapes (type-only surface, 2-level__exportStarchain, no-default ESM member call) — compile, link, run, byte-exact output.cargo test --release -p perrygreen, 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).hono-basicPASS,node-http-servePASS.axios-get/ws-echofail identically with the pre-change baseline binary (pre-existing, unrelated).cargo fmt --all -- --checkandscripts/check_file_size.shclean.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).