From e2d4092b16dfdac1879ff01b5893efcc88871f9a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 13:18:18 -0700 Subject: [PATCH] Re-export classes through module.exports members that require them `module.exports.Agent = require('./lib/agent')`, where agent.js ends with `module.exports = Agent`, is the same alias plumbing as `module.exports.Agent = Agent`: importers resolve to the class declaration and the class registry applies, instead of storing a class value (which classes inheriting a runtime-provided constructor, such as EventEmitter subclasses, do not have). The statement still evaluates the required module at its position. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 5 +++- .../compiler/src/frontend/lowering/lowerer.ts | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 36abb9130..6feccd5a5 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6168,7 +6168,10 @@ function lowerCjsExportStatement( // storage — collectGlobals skipped it; importers re-resolve through // cjsMemberExportClassSymbol): the statement lowers to nothing. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) { - return { kind: "block", body: [], loc }; + // An inline require still evaluates its module here. + const req = inlineProgramRequireOf(lowerer, cjs.value); + const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null; + return init ?? { kind: "block", body: [], loc }; } // The tsc VOID-INIT PREAMBLE (`exports.leaf = void 0;` with the real // `exports.leaf = leaf;` further down — every tsc-emitted dist file diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4ecb0176b..aa05b734e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -294,6 +294,7 @@ import { import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; import { cjsNamespaceModule, + cjsWholeExportValueOf, inlineNamespaceRequireOf, inlineProgramRequireOf, } from "./cjs-inline-require.js"; @@ -3672,13 +3673,18 @@ export class Lowerer { ? d.parent : null; if (!assign || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; - if (!ts.isIdentifier(assign.right)) return null; const stmt = assign.parent; if (!stmt || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null; if (!isJsSourceFile(stmt.parent)) return null; const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind !== "member" || cjs.expr !== assign) return null; if (cjsExportDiscardReason(stmt) !== null) return null; + // `module.exports.C = require('./c')` over a module whose whole export + // is a class identifier: the same alias plumbing; the statement keeps + // the require's module evaluation at its position. + const required = this.memberExportRequiredClass(assign.right); + if (required !== undefined) return required; + if (!ts.isIdentifier(assign.right)) return null; let value = this.checker.getSymbolAtLocation(assign.right); if (!value) return null; if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); @@ -3686,6 +3692,22 @@ export class Lowerer { return isClass ? value : null; } + /** For an inline program `require("./c")`: the class symbol its module's + * kept `module.exports = ` names, or null when the + * whole export is something else. Undefined when `expr` is not such a + * require. */ + memberExportRequiredClass(expr: ts.Expression): ts.Symbol | null | undefined { + const req = inlineProgramRequireOf(this, expr); + if (req === null) return undefined; + const whole = cjsWholeExportValueOf(req.dep); + if (whole === null || !ts.isIdentifier(whole.rhs)) return null; + let value = this.checker.getSymbolAtLocation(whole.rhs); + if (!value) return null; + if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); + const isClass = this.checker.declarationsOf(value).some((decl) => ts.isClassDeclaration(decl)); + return isClass ? value : null; + } + /** The CommonJS JS module a DEFAULT-import binding's declaration loads * (`import d from "./lib.cjs"`), or null: Node's ESM-CJS interop binds * the default to module.exports — exactly a require binding — so those