## v0.5.839 — fix(cjs-wrap): #665 — `class Child extends Parent` across cjs-wrapped modules now runs the parent constructor instead of silently no-op'ing. **Symptom.** Under v0.5.836's "object-literal aggregator forwarding" fix, the minimal class-identity test passed (`new Other(...)` works, `o.hello()` is a function), but child classes that `extend` an imported parent across cjs-wrapped modules had `super(opts)` produce no observable effect: the parent's constructor body never executed, and any fields the parent's body would set (`this.points = opts.points`, etc.) showed up as `undefined`-valued keys on the instance. This is the rate-limiter-flexible `RateLimiterMemory extends RateLimiterAbstract` shape that has continued blocking the user's native-server boot path through three rounds of "fix attempts" since #652. The previous fix correctly forwarded class identity through `module.exports = { X, Y }` aggregators, but the parent constructor body remained silently unreachable. **Root cause.** When cjs_wrap encounters `var X = require('./Y')` inside a leaf module (e.g. `RateLimiterMemory.js`'s `const RateLimiterAbstract = require('./RateLimiterAbstract')`), it has to surface a binding for `X` at module scope so the hoisted `class Child extends X` declaration above the IIFE can resolve `X`. Pre-fix the wrap emitted `import _req_0 from './Y'; const X = _req_0;` — `_req_0` is the import local, `X` is a const aliasing it. compile.rs's default-import handler then registered `imported_class_ctors["_req_0"]` and `imported_class_ctors["default"]` but NOT `imported_class_ctors["X"]` — the const alias is invisible to the import tracking. The codegen super-call dispatch at `crates/perry-codegen/src/expr.rs:5094` looks up `parent_class.extends_name` (which is `"X"` per HIR's class lowering at `lower_decl.rs:1570`) in `ctx.classes`. For an imported parent the lookup hits the no-parent-in-ctx branch at line 4836 — the `imported_class_ctors` cross-module ctor dispatch at line 5094 is INSIDE the parent-found block and therefore unreachable. Falls through to the stream/Error-like fallback chain → `return Ok(double_literal(TAG_UNDEFINED))` at line 4933. `super(opts)` becomes a static no-op. The previous CJS-wrap landings (#488 drizzle, #487 chrono, #652 / #665 ESM classes) all worked because they didn't combine **cross-module class inheritance** with the **wrap-emitted const-alias indirection** — the prior failures stopped earlier in the pipeline. **Fix.** Single point of change in `crates/perry/src/commands/compile/cjs_wrap.rs::wrap_commonjs`: when a CJS source has a unique `var/const/let X = require('Y')` alias and the alias name is "safe" (not `_cjs`, `module`, `exports`, `require`, `_req_*`, and not a hoisted class name from the same file), use X as the import local name directly instead of `_req_N`. The wrap emits `import X from 'Y'` (no separate const) and the IIFE's `require('Y')` returns `X`. compile.rs's default-import handler then registers `imported_class_ctors["X"]` via the existing local_alias path at line 3222–3272, codegen's `imported_class_stubs` builder at `codegen.rs:512–594` registers `ctx.classes["X"]` as a stub Class, and the super-call dispatch's parent lookup succeeds → falls into the existing line-5094 branch which calls the source module's standalone `perry_<srcprefix>__X_constructor` symbol. That standalone ctor itself runs `apply_field_initializers_recursive_pub`, so all parent-class arrow-field initializers (`request = (...) => ...`, `fetch = (...) => ...` on hono-base, etc.) ALSO land on `this`. The first safe alias per spec wins; subsequent aliases of the same spec keep their `const X = <chosen_local>;` form. **Validation.** **Minimal aggregator + extends repro (rate-limiter-flexible shape)**: 5 files modeling `index.js` (aggregator), `lib/RateLimiterMemory.js` (child class with `super(opts)`), `lib/RateLimiterAbstract.js` (parent with `this.points = opts.points`). Pre-fix: `[Abstract.ctor]` logs never appear, `this.points` is undefined inside `consume()`, `describe()` returns `"abstract:undefined/undefined"`. Post-fix: `[Abstract.ctor] after this.points= 3 / after this.duration= 60`, `consume:ip:1.2.3.4:3`, `describe: abstract:3/60` — byte-identical to `node --experimental-strip-types`. **The user's exact third-rerun rlfshape probe** (`{node_modules/rlfshape/{package.json,index.js,lib/Other.js},package.json,main.ts}` with `compilePackages: ["rlfshape"]`): pre-fix segfaulted with `typeof o: undefined`, post-fix prints `typeof o: object / Object.keys(o): _x / typeof o.hello: function / hello: hi from 99 / exit=0` — byte-identical to `tsx main.ts`. **Three existing cjs_wrap unit tests** updated to match the new emit shape: `wrap_hoists_require_as_import` (was: `import _req_0 from './dep'`; now: `import dep from './dep'`), `wrap_aliases_import_for_hoisted_class_extends_and_strips_iife_var` (was: looks for `const import_dep = _req_0;` module-scope alias; now: looks for `import import_dep from './dep.cjs';`), `wrap_emits_direct_reexport_for_object_literal_aggregator` (was: `export { _req_0 as RateLimiterMemory };`; now: `export { RateLimiterMemory as RateLimiterMemory };` — the alias IS the import local). One new defensive test (`wrap_falls_back_to_req_n_when_alias_unsafe`) verifies the `_req_N` fallback when the alias would collide with a wrap-internal binding (`var _cjs = require('./a')` → keeps `_req_0`). Full cjs_wrap suite: 31/31 PASS (28 existing + 3 updated). Full perry package suite: 146/146 PASS. **Real `rate-limiter-flexible` from npm**: the issue's blocker — `import { RateLimiterMemory } from "rate-limiter-flexible"; new RateLimiterMemory({...}).consume(...)` — now reaches `RateLimiterAbstract`'s constructor body for the first time. A separate downstream issue with that package's **getter/setter accessor pattern** (`get points() { return this._points; } set points(v) { this._points = v >= 0 ? v : 4; }`) surfaces next: `limiter.points` reads as 0 instead of 3 because cross-module accessor dispatch for imported classes isn't yet wired. That's a distinct bug worth its own focused issue — `super()` field-propagation through getter/setter pairs across imported parents — but unrelated to the `super()` no-op gap this commit closes. **Out of scope.** The same wrap-emit pattern also lets the compile-package consumer reach the parent's *method* bodies through `super.foo(...)` calls, but no real-world test of that surfaced in this session. Accessor-pair dispatch across imported classes is the next blocker for rate-limiter-flexible specifically.
0 commit comments