Skip to content

Class methods nested in a function or CommonJS module see a stale snapshot of enclosing vars assigned after the class (TypeScript var _a; … _a = X emit) #10485

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. When a class is declared in a function body, including the body of a
compiled CommonJS module, its methods see the value an enclosing var had when the class was defined. Later
assignments are not seen. let bindings and plain closures are live. The most common victim is TypeScript's emit for
a class whose static members refer to the class itself (var _a; class X {...} _a = X;): _a stays undefined
inside the static methods.

Reproduction

package.json

{"name":"repro","private":true,"type":"module"}

lib.cjs (the shape TypeScript emits when static members of a class refer to the class itself)

"use strict";
var _a;
class Client {
  static create(x) { return _a.factory(x); }
  static factory(x) { return 'factory(' + x + ')'; }
}
_a = Client;
exports.Client = Client;

main.ts

import lib from './lib.cjs';
try { console.log('CJS static via _a:', (lib as any).Client.create(1)); } catch (e: any) { console.log('CJS static via _a: THREW', e.message); }

function scope() {
  var v = 1;
  let l = 1;
  var u: any;
  class K {
    static s() { return JSON.stringify([v, l, u]); }
    i() { return JSON.stringify([v, l, u]); }
  }
  v = 2; l = 2; u = 'set';
  const fn = () => JSON.stringify([v, l, u]);
  console.log('after assignment: static', K.s(), '| instance', new K().i(), '| arrow', fn());
  v = 3; l = 3; u = 'again';
  console.log('after 2nd assign: static', K.s(), '| instance', new K().i(), '| arrow', fn());
}
scope();
node main.ts
perry compile main.ts -o out && ./out

Expected (Node 26.5.1)

CJS static via _a: factory(1)
after assignment: static [2,2,"set"] | instance [2,2,"set"] | arrow [2,2,"set"]
after 2nd assign: static [3,3,"again"] | instance [3,3,"again"] | arrow [3,3,"again"]

Actual (Perry)

Same with and without PERRY_NO_AUTO_OPTIMIZE=1:

CJS static via _a: THREW Cannot read properties of undefined (reading 'factory')
after assignment: static [1,2,null] | instance [1,2,"set"] | arrow [2,2,"set"]
after 2nd assign: static [1,3,null] | instance [1,3,"again"] | arrow [3,3,"again"]

Impact

  • @redis/client 6.1.0: dist/lib/client/index.js:116 (static create(options) { return _a.factory(options)(options); },
    with _a = RedisClient; at line 1194) throws Cannot read properties of undefined (reading 'factory'). The same
    var _a … _a = X emit is in 5 files of @redis/client (lib/client/index.js, lib/client/enterprise-maintenance-manager.js,
    lib/cluster/cluster-slots.js, lib/cluster/cluster-reconnection-tracker.js, lib/RESP/decoder.js).
  • Any TypeScript-compiled CommonJS package using this emit, and any CJS/bundled code that declares a class before
    assigning a var it reads.

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
  2. added a commit that references this issue on Sep 18, 2026
    a669455
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