Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support ESM module.exports named exports
  • Loading branch information
andrewbranch committed Jun 2, 2025
commit 218d141a0e38316e0dfc831b9e3bdd780ff74554
73 changes: 61 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3683,6 +3683,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
getExternalModuleRequireArgument(node) || getExternalModuleImportEqualsDeclarationExpression(node),
);
const resolved = resolveExternalModuleSymbol(immediate);
if (resolved && ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext) {
const moduleExports = getExportOfModule(resolved, "module.exports" as __String, node, dontResolveAlias);
if (moduleExports) {
return moduleExports;
}
}
Comment thread
andrewbranch marked this conversation as resolved.
Resolved
markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false);
return resolved;
}
Expand Down Expand Up @@ -3798,16 +3804,44 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getTargetofModuleDefault(moduleSymbol: Symbol, node: ImportClause | ImportOrExportSpecifier, dontResolveAlias: boolean) {
const file = moduleSymbol.declarations?.find(isSourceFile);
const specifier = getModuleSpecifierForImportOrExport(node);
let exportDefaultSymbol: Symbol | undefined;
let exportModuleDotExportsSymbol: Symbol | undefined;
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
exportDefaultSymbol = moduleSymbol;
}
else if (
file && specifier &&
ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext &&
getEmitSyntaxForModuleSpecifierExpression(specifier) === ModuleKind.CommonJS &&
host.getImpliedNodeFormatForEmit(file) === ModuleKind.ESNext &&
(exportModuleDotExportsSymbol = resolveExportByName(moduleSymbol, "module.exports" as __String, node, dontResolveAlias))
) {
// We have a transpiled default import where the `require` resolves to an ES module with a `module.exports` named
// export. If `esModuleInterop` is enabled, this will work:
//
// const dep_1 = __importDefault(require("./dep.mjs")); // wraps like { default: require("./dep.mjs") }
// dep_1.default; // require("./dep.mjs") -> the `module.exports` export value
//
// But without `esModuleInterop`, it will be broken:
//
// const dep_1 = require("./dep.mjs"); // the `module.exports` export value (could be primitive)
// dep_1.default; // `default` property access on the `module.exports` export value
//
// We could try to resolve the 'default' property in the latter case, but it's a mistake to run in this
// environment without `esModuleInterop`, so just error.
if (!getESModuleInterop(compilerOptions)) {
error(node.name, Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), "esModuleInterop");
return undefined;
}
markSymbolOfAliasDeclarationIfTypeOnly(node, exportModuleDotExportsSymbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ false);
return exportModuleDotExportsSymbol;
}
else {
exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias);
}

const file = moduleSymbol.declarations?.find(isSourceFile);
const specifier = getModuleSpecifierForImportOrExport(node);
if (!specifier) {
return exportDefaultSymbol;
}
Expand Down Expand Up @@ -4953,10 +4987,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

const referenceParent = referencingLocation.parent;
if (
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
isImportCall(referenceParent)
) {
const namespaceImport = isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent);
if (namespaceImport || isImportCall(referenceParent)) {
const reference = isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier;
const type = getTypeOfSymbol(symbol);
const defaultOnlyType = getTypeWithSyntheticDefaultOnly(type, symbol, moduleSymbol!, reference);
Expand All @@ -4965,14 +4997,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

const targetFile = moduleSymbol?.declarations?.find(isSourceFile);
const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(getEmitSyntaxForModuleSpecifierExpression(reference), host.getImpliedNodeFormatForEmit(targetFile));
if (getESModuleInterop(compilerOptions) || isEsmCjsRef) {
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
if (!sigs || !sigs.length) {
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
const usageMode = getEmitSyntaxForModuleSpecifierExpression(reference);
let exportModuleDotExportsSymbol: Symbol | undefined;
if (
namespaceImport && targetFile &&
ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext &&
usageMode === ModuleKind.CommonJS && host.getImpliedNodeFormatForEmit(targetFile) === ModuleKind.ESNext &&
(exportModuleDotExportsSymbol = resolveExportByName(symbol, "module.exports" as __String, namespaceImport, dontResolveAlias))
) {
if (!suppressInteropError && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
error(referencingLocation, Diagnostics.This_module_can_only_be_referenced_with_ECMAScript_imports_Slashexports_by_turning_on_the_0_flag_and_referencing_its_default_export, "esModuleInterop");
}
if (getESModuleInterop(compilerOptions) && hasSignatures(type)) {
return cloneTypeAsModuleType(exportModuleDotExportsSymbol, type, referenceParent);
}
return exportModuleDotExportsSymbol;
}

const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(usageMode, host.getImpliedNodeFormatForEmit(targetFile));
if (getESModuleInterop(compilerOptions) || isEsmCjsRef) {
if (
(sigs && sigs.length) ||
hasSignatures(type) ||
getPropertyOfType(type, InternalSymbolName.Default, /*skipObjectFunctionPropertyAugment*/ true) ||
isEsmCjsRef
) {
Expand All @@ -4987,6 +5032,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return symbol;
}

function hasSignatures(type: Type): boolean {
return some(getSignaturesOfStructuredType(type, SignatureKind.Call)) || some(getSignaturesOfStructuredType(type, SignatureKind.Construct));
}

/**
* Create a new symbol which has the module's type less the call and construct signatures
*/
Expand Down
24 changes: 24 additions & 0 deletions tests/baselines/reference/esmModuleExports1.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/importer-cts.cts(10,10): error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?


==== /importer-cjs.cjs (0 errors) ====
const Foo = require("./exporter.mjs");
new Foo();

==== /importer-cts.cts (1 errors) ====
import Foo = require("./exporter.mjs");
new Foo();

import Foo2 from "./exporter.mjs";
new Foo2();

import * as Foo3 from "./exporter.mjs";
new Foo3();

import { Oops } from "./exporter.mjs";
~~~~
!!! error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?

==== /exporter.mts (0 errors) ====
export default class Foo {}
export { Foo as "module.exports" };
41 changes: 41 additions & 0 deletions tests/baselines/reference/esmModuleExports1.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//// [tests/cases/conformance/node/esmModuleExports1.ts] ////

=== /importer-cjs.cjs ===
const Foo = require("./exporter.mjs");
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
>require : Symbol(require)
>"./exporter.mjs" : Symbol("/exporter", Decl(exporter.mts, 0, 0))

new Foo();
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))

=== /importer-cts.cts ===
import Foo = require("./exporter.mjs");
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))

new Foo();
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))

import Foo2 from "./exporter.mjs";
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))

new Foo2();
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))

import * as Foo3 from "./exporter.mjs";
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))

new Foo3();
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))

import { Oops } from "./exporter.mjs";
>Oops : Symbol(Oops, Decl(importer-cts.cts, 9, 8))

=== /exporter.mts ===
export default class Foo {}
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))

export { Foo as "module.exports" };
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))
>"module.exports" : Symbol("module.exports", Decl(exporter.mts, 1, 8))

65 changes: 65 additions & 0 deletions tests/baselines/reference/esmModuleExports1.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//// [tests/cases/conformance/node/esmModuleExports1.ts] ////

=== /importer-cjs.cjs ===
const Foo = require("./exporter.mjs");
>Foo : typeof Foo
> : ^^^^^^^^^^
>require("./exporter.mjs") : typeof import("/exporter", { with: { "resolution-mode": "import" } })
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>require : any
> : ^^^
>"./exporter.mjs" : "./exporter.mjs"
> : ^^^^^^^^^^^^^^^^

new Foo();
>new Foo() : Foo
> : ^^^
>Foo : typeof Foo
> : ^^^^^^^^^^

=== /importer-cts.cts ===
import Foo = require("./exporter.mjs");
>Foo : typeof Foo
> : ^^^^^^^^^^

new Foo();
>new Foo() : Foo
> : ^^^
>Foo : typeof Foo
> : ^^^^^^^^^^

import Foo2 from "./exporter.mjs";
>Foo2 : typeof Foo
> : ^^^^^^^^^^

new Foo2();
>new Foo2() : Foo
> : ^^^
>Foo2 : typeof Foo
> : ^^^^^^^^^^

import * as Foo3 from "./exporter.mjs";
>Foo3 : typeof Foo
> : ^^^^^^^^^^

new Foo3();
>new Foo3() : Foo
> : ^^^
>Foo3 : typeof Foo
> : ^^^^^^^^^^

import { Oops } from "./exporter.mjs";
>Oops : any
> : ^^^

=== /exporter.mts ===
export default class Foo {}
>Foo : Foo
> : ^^^

export { Foo as "module.exports" };
>Foo : typeof Foo
> : ^^^^^^^^^^
>"module.exports" : typeof Foo
> : ^^^^^^^^^^

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/importer-cjs.cjs(2,5): error TS2351: This expression is not constructable.
Type 'String' has no construct signatures.
/importer-cts.cts(2,5): error TS2351: This expression is not constructable.
Type 'String' has no construct signatures.
/importer-cts.cts(4,8): error TS1259: Module '"/exporter"' can only be default-imported using the 'esModuleInterop' flag
/importer-cts.cts(8,5): error TS2351: This expression is not constructable.
Type 'String' has no construct signatures.
/importer-cts.cts(10,10): error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?


==== /importer-cjs.cjs (1 errors) ====
const Foo = require("./exporter.mjs");
new Foo();
~~~
!!! error TS2351: This expression is not constructable.
!!! error TS2351: Type 'String' has no construct signatures.

==== /importer-cts.cts (4 errors) ====
import Foo = require("./exporter.mjs");
new Foo();
~~~
!!! error TS2351: This expression is not constructable.
!!! error TS2351: Type 'String' has no construct signatures.

import Foo2 from "./exporter.mjs";
~~~~
!!! error TS1259: Module '"/exporter"' can only be default-imported using the 'esModuleInterop' flag
new Foo2();

import * as Foo3 from "./exporter.mjs";
new Foo3();
~~~~
!!! error TS2351: This expression is not constructable.
!!! error TS2351: Type 'String' has no construct signatures.

import { Oops } from "./exporter.mjs";
~~~~
!!! error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?

==== /exporter.mts (0 errors) ====
export default class Foo {}
const oops = "oops";
export { oops as "module.exports" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//// [tests/cases/conformance/node/esmModuleExports2.ts] ////

=== /importer-cjs.cjs ===
const Foo = require("./exporter.mjs");
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
>require : Symbol(require)
>"./exporter.mjs" : Symbol("/exporter", Decl(exporter.mts, 0, 0))

new Foo();
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))

=== /importer-cts.cts ===
import Foo = require("./exporter.mjs");
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))

new Foo();
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))

import Foo2 from "./exporter.mjs";
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))

new Foo2();
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))

import * as Foo3 from "./exporter.mjs";
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))

new Foo3();
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))

import { Oops } from "./exporter.mjs";
>Oops : Symbol(Oops, Decl(importer-cts.cts, 9, 8))

=== /exporter.mts ===
export default class Foo {}
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))

const oops = "oops";
>oops : Symbol(oops, Decl(exporter.mts, 1, 5))

export { oops as "module.exports" };
>oops : Symbol(oops, Decl(exporter.mts, 1, 5))
>"module.exports" : Symbol("module.exports", Decl(exporter.mts, 2, 8))

Loading