Skip to content

post-#652: CJS module.exports = Class + extends still drops methods (rate-limiter-flexible repro) #665

Description

@proggeramlug

Following up on #652 (closed in v0.5.795 via dee8659): the ESM export class shape is now fixed — @perryts/mysql's Pool instantiates and pool.query(...) is a function. But the CJS module.exports = Class + extends shape still crashes:

TypeError: value is not a function
    at <anonymous>

Hit this trying to use rate-limiter-flexible's RateLimiterMemory from a Perry-compiled native server: every method on the instance is undefined (typeof limiter.consume === "undefined"), and our request handler crashes with the TypeError above before any HTTP body is written.

Reproducer (3 files, total ~12 lines of code)

// /tmp/cjs-class/base.js
class Base {
  greetBase(n) { return "base:" + n; }
}
module.exports = Base;
// /tmp/cjs-class/child.js
const Base = require('./base');
class Child extends Base {
  constructor(opts) { super(opts); this.opts = opts; }
  greetChild(n) { return "child:" + n; }
}
module.exports = Child;
// /tmp/cjs-class/main.ts
const Child = require('./child');
async function main() {
  const c = new Child({ x: 1 });
  console.log("typeof c.greetChild:", typeof c.greetChild);
  console.log("typeof c.greetBase:",  typeof c.greetBase);
  if (typeof c.greetChild === "function") console.log(c.greetChild("hi"));
  if (typeof c.greetBase  === "function") console.log(c.greetBase("hi"));
}
main().catch((e) => { console.error(e); process.exit(2); });
$ perry compile main.ts -o probe && ./probe
TypeError: value is not a function
    at <anonymous>
$ echo $?
1

The crash fires during new Child({ x: 1 }) itself — before either typeof log can run. So it's not just the methods that vanish; the construction path bails too. Same code under tsx main.ts works correctly:

$ npx tsx main.ts
typeof c.greetChild: function
typeof c.greetBase:  function
child:hi
base:hi

Difference from the original #652 case

@perryts/mysql's Pool is an ESM module: import { connect } from './connection'; export class Pool { ... } with bare class fields and no inheritance. dee8659's cjs_wrap fix unblocked that.

rate-limiter-flexible's RateLimiterMemory (and the published shape of many older npm packages) is CommonJS:

const RateLimiterAbstract = require('./RateLimiterAbstract');
class RateLimiterMemory extends RateLimiterAbstract {
  constructor(opts = {}) { super(opts); ... }
  consume(key, ...) { ... }
}
module.exports = RateLimiterMemory;

Three things stack here that the ESM repro didn't have:

  1. module.exports = Class (single-export, no destructuring on the consumer side)
  2. class … extends Base
  3. require('./relative-cjs-file') returning the bare class

Any one of those may be the trigger; they appear together in rate-limiter-flexible's output.

Discovery context

Native server boots through migrations + crypto KID + listening (#609 + #639 + #652 ESM-class fix all good ✓), serves /healthz / /readyz / /metrics correctly, but the very first POST /v1/auth/signup dies in our rate-limit middleware:

await signupLimiter.consume(ipKey(req, "signup"));   // ← TypeError here

signupLimiter is new RateLimiterMemory({...}). With the precompiled-CJS instance methods missing, .consume is undefined.

Same code under tsx server/main.ts runs the entire 24-suite e2e green. Only the perry-compiled native binary path fails.

Environment

  • perry 0.5.795 (target/release perry-runtime + perry-stdlib rebuilt from dee86595)
  • rate-limiter-flexible@5.0.x (precompiled CJS in node_modules)
  • --target macos, arm64

Why we filed

After your dee8659 ESM-class fix, the native server made it all the way through boot for the first time — major progress. This CJS variant blocks it on the very first authed request. Once it lands, every other CJS-published npm package's classes (most of the ecosystem) becomes usable from Perry-compiled binaries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions