Skip to content

Class expression returned from a function in a non-entry module has no per-evaluation identity: every call returns the same class, and the last extends wins #10455

Closed

Description

@proggeramlug

Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A mixin/factory such as function withCommands(Base) { return class extends Base {} }
works when it is declared in the entry module. Declared in any other module, every call returns the same class
object, re-parented to the most recent Base. Instances of an earlier result run the wrong parent constructor and
lose the parent's prototype.

Reproduction

factory.ts

export function withCommands(Base: any) { return class extends Base {}; }

main.ts

import { withCommands } from "./factory.ts";
class A { constructor() { console.log("  A constructor"); } hello() { return "A.hello"; } }
class B { constructor() { console.log("  B constructor"); } }
function localWithCommands(Base: any) { return class extends Base {}; }

const LocalA = localWithCommands(A), LocalB = localWithCommands(B);
const la = new LocalA();
console.log("factory in main.ts:    LocalA !== LocalB:", LocalA !== LocalB, "| instanceof A:", la instanceof A, "| typeof hello:", typeof la.hello);

const ClientA = withCommands(A), ClientB = withCommands(B);
const ca = new ClientA();
console.log("factory in factory.ts: ClientA !== ClientB:", ClientA !== ClientB, "| instanceof A:", ca instanceof A, "| typeof hello:", typeof ca.hello);
node main.ts
PERRY_NO_AUTO_OPTIMIZE=1 perry compile main.ts -o out && ./out   # same output with default auto-optimize

Expected (Node 26.5.1)

  A constructor
factory in main.ts:    LocalA !== LocalB: true | instanceof A: true | typeof hello: function
  A constructor
factory in factory.ts: ClientA !== ClientB: true | instanceof A: true | typeof hello: function

Actual (Perry)

  A constructor
factory in main.ts:    LocalA !== LocalB: true | instanceof A: true | typeof hello: function
  B constructor
factory in factory.ts: ClientA !== ClientB: false | instanceof A: false | typeof hello: undefined

Impact

  • redis 6.1.0 / @redis/client 6.1.0: attachConfig (dist/lib/commander.js:5-8, Class = class extends BaseClass {})
    is called from RedisClient.factory with BaseClass: RedisClient (dist/lib/client/index.js:98). It is called
    again right away with BaseClass: RedisClientMultiCommand through Client.prototype.Multi = multi_command_1.default.extend(config)
    (index.js:107 → dist/lib/client/multi-command.js:54). The later new Client(options) (index.js:112) then runs
    the RedisClientMultiCommand constructor instead of RedisClient's. createClient() returns an object that is not
    instanceof RedisClient and has no connect, so client.connect() fails. The same factory also serves
    client/pool.js:62, cluster/index.js:83, cluster/multi-command.js:58 and sentinel/index.js:70,239.
  • Any mixin/factory-class pattern in a library module is affected. Mixins are common in ORMs, Effect/zod-style
    $constructor factories and command builders.

Notes

Activity

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions