Skip to content

Commit 218d141

Browse files
committed
Support ESM module.exports named exports
1 parent 8e14aa4 commit 218d141

16 files changed

+727
-12
lines changed

src/compiler/checker.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3683,6 +3683,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
36833683
getExternalModuleRequireArgument(node) || getExternalModuleImportEqualsDeclarationExpression(node),
36843684
);
36853685
const resolved = resolveExternalModuleSymbol(immediate);
3686+
if (resolved && ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext) {
3687+
const moduleExports = getExportOfModule(resolved, "module.exports" as __String, node, dontResolveAlias);
3688+
if (moduleExports) {
3689+
return moduleExports;
3690+
}
3691+
}
36863692
markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false);
36873693
return resolved;
36883694
}
@@ -3798,16 +3804,44 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
37983804
}
37993805

38003806
function getTargetofModuleDefault(moduleSymbol: Symbol, node: ImportClause | ImportOrExportSpecifier, dontResolveAlias: boolean) {
3807+
const file = moduleSymbol.declarations?.find(isSourceFile);
3808+
const specifier = getModuleSpecifierForImportOrExport(node);
38013809
let exportDefaultSymbol: Symbol | undefined;
3810+
let exportModuleDotExportsSymbol: Symbol | undefined;
38023811
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
38033812
exportDefaultSymbol = moduleSymbol;
38043813
}
3814+
else if (
3815+
file && specifier &&
3816+
ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext &&
3817+
getEmitSyntaxForModuleSpecifierExpression(specifier) === ModuleKind.CommonJS &&
3818+
host.getImpliedNodeFormatForEmit(file) === ModuleKind.ESNext &&
3819+
(exportModuleDotExportsSymbol = resolveExportByName(moduleSymbol, "module.exports" as __String, node, dontResolveAlias))
3820+
) {
3821+
// We have a transpiled default import where the `require` resolves to an ES module with a `module.exports` named
3822+
// export. If `esModuleInterop` is enabled, this will work:
3823+
//
3824+
// const dep_1 = __importDefault(require("./dep.mjs")); // wraps like { default: require("./dep.mjs") }
3825+
// dep_1.default; // require("./dep.mjs") -> the `module.exports` export value
3826+
//
3827+
// But without `esModuleInterop`, it will be broken:
3828+
//
3829+
// const dep_1 = require("./dep.mjs"); // the `module.exports` export value (could be primitive)
3830+
// dep_1.default; // `default` property access on the `module.exports` export value
3831+
//
3832+
// We could try to resolve the 'default' property in the latter case, but it's a mistake to run in this
3833+
// environment without `esModuleInterop`, so just error.
3834+
if (!getESModuleInterop(compilerOptions)) {
3835+
error(node.name, Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), "esModuleInterop");
3836+
return undefined;
3837+
}
3838+
markSymbolOfAliasDeclarationIfTypeOnly(node, exportModuleDotExportsSymbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ false);
3839+
return exportModuleDotExportsSymbol;
3840+
}
38053841
else {
38063842
exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias);
38073843
}
38083844

3809-
const file = moduleSymbol.declarations?.find(isSourceFile);
3810-
const specifier = getModuleSpecifierForImportOrExport(node);
38113845
if (!specifier) {
38123846
return exportDefaultSymbol;
38133847
}
@@ -4953,10 +4987,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
49534987
}
49544988

49554989
const referenceParent = referencingLocation.parent;
4956-
if (
4957-
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
4958-
isImportCall(referenceParent)
4959-
) {
4990+
const namespaceImport = isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent);
4991+
if (namespaceImport || isImportCall(referenceParent)) {
49604992
const reference = isImportCall(referenceParent) ? referenceParent.arguments[0] : referenceParent.moduleSpecifier;
49614993
const type = getTypeOfSymbol(symbol);
49624994
const defaultOnlyType = getTypeWithSyntheticDefaultOnly(type, symbol, moduleSymbol!, reference);
@@ -4965,14 +4997,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
49654997
}
49664998

49674999
const targetFile = moduleSymbol?.declarations?.find(isSourceFile);
4968-
const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(getEmitSyntaxForModuleSpecifierExpression(reference), host.getImpliedNodeFormatForEmit(targetFile));
4969-
if (getESModuleInterop(compilerOptions) || isEsmCjsRef) {
4970-
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
4971-
if (!sigs || !sigs.length) {
4972-
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
5000+
const usageMode = getEmitSyntaxForModuleSpecifierExpression(reference);
5001+
let exportModuleDotExportsSymbol: Symbol | undefined;
5002+
if (
5003+
namespaceImport && targetFile &&
5004+
ModuleKind.Node20 <= moduleKind && moduleKind <= ModuleKind.NodeNext &&
5005+
usageMode === ModuleKind.CommonJS && host.getImpliedNodeFormatForEmit(targetFile) === ModuleKind.ESNext &&
5006+
(exportModuleDotExportsSymbol = resolveExportByName(symbol, "module.exports" as __String, namespaceImport, dontResolveAlias))
5007+
) {
5008+
if (!suppressInteropError && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
5009+
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");
49735010
}
5011+
if (getESModuleInterop(compilerOptions) && hasSignatures(type)) {
5012+
return cloneTypeAsModuleType(exportModuleDotExportsSymbol, type, referenceParent);
5013+
}
5014+
return exportModuleDotExportsSymbol;
5015+
}
5016+
5017+
const isEsmCjsRef = targetFile && isESMFormatImportImportingCommonjsFormatFile(usageMode, host.getImpliedNodeFormatForEmit(targetFile));
5018+
if (getESModuleInterop(compilerOptions) || isEsmCjsRef) {
49745019
if (
4975-
(sigs && sigs.length) ||
5020+
hasSignatures(type) ||
49765021
getPropertyOfType(type, InternalSymbolName.Default, /*skipObjectFunctionPropertyAugment*/ true) ||
49775022
isEsmCjsRef
49785023
) {
@@ -4987,6 +5032,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
49875032
return symbol;
49885033
}
49895034

5035+
function hasSignatures(type: Type): boolean {
5036+
return some(getSignaturesOfStructuredType(type, SignatureKind.Call)) || some(getSignaturesOfStructuredType(type, SignatureKind.Construct));
5037+
}
5038+
49905039
/**
49915040
* Create a new symbol which has the module's type less the call and construct signatures
49925041
*/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/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?
2+
3+
4+
==== /importer-cjs.cjs (0 errors) ====
5+
const Foo = require("./exporter.mjs");
6+
new Foo();
7+
8+
==== /importer-cts.cts (1 errors) ====
9+
import Foo = require("./exporter.mjs");
10+
new Foo();
11+
12+
import Foo2 from "./exporter.mjs";
13+
new Foo2();
14+
15+
import * as Foo3 from "./exporter.mjs";
16+
new Foo3();
17+
18+
import { Oops } from "./exporter.mjs";
19+
~~~~
20+
!!! error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?
21+
22+
==== /exporter.mts (0 errors) ====
23+
export default class Foo {}
24+
export { Foo as "module.exports" };
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//// [tests/cases/conformance/node/esmModuleExports1.ts] ////
2+
3+
=== /importer-cjs.cjs ===
4+
const Foo = require("./exporter.mjs");
5+
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
6+
>require : Symbol(require)
7+
>"./exporter.mjs" : Symbol("/exporter", Decl(exporter.mts, 0, 0))
8+
9+
new Foo();
10+
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
11+
12+
=== /importer-cts.cts ===
13+
import Foo = require("./exporter.mjs");
14+
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))
15+
16+
new Foo();
17+
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))
18+
19+
import Foo2 from "./exporter.mjs";
20+
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))
21+
22+
new Foo2();
23+
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))
24+
25+
import * as Foo3 from "./exporter.mjs";
26+
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))
27+
28+
new Foo3();
29+
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))
30+
31+
import { Oops } from "./exporter.mjs";
32+
>Oops : Symbol(Oops, Decl(importer-cts.cts, 9, 8))
33+
34+
=== /exporter.mts ===
35+
export default class Foo {}
36+
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))
37+
38+
export { Foo as "module.exports" };
39+
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))
40+
>"module.exports" : Symbol("module.exports", Decl(exporter.mts, 1, 8))
41+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//// [tests/cases/conformance/node/esmModuleExports1.ts] ////
2+
3+
=== /importer-cjs.cjs ===
4+
const Foo = require("./exporter.mjs");
5+
>Foo : typeof Foo
6+
> : ^^^^^^^^^^
7+
>require("./exporter.mjs") : typeof import("/exporter", { with: { "resolution-mode": "import" } })
8+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9+
>require : any
10+
> : ^^^
11+
>"./exporter.mjs" : "./exporter.mjs"
12+
> : ^^^^^^^^^^^^^^^^
13+
14+
new Foo();
15+
>new Foo() : Foo
16+
> : ^^^
17+
>Foo : typeof Foo
18+
> : ^^^^^^^^^^
19+
20+
=== /importer-cts.cts ===
21+
import Foo = require("./exporter.mjs");
22+
>Foo : typeof Foo
23+
> : ^^^^^^^^^^
24+
25+
new Foo();
26+
>new Foo() : Foo
27+
> : ^^^
28+
>Foo : typeof Foo
29+
> : ^^^^^^^^^^
30+
31+
import Foo2 from "./exporter.mjs";
32+
>Foo2 : typeof Foo
33+
> : ^^^^^^^^^^
34+
35+
new Foo2();
36+
>new Foo2() : Foo
37+
> : ^^^
38+
>Foo2 : typeof Foo
39+
> : ^^^^^^^^^^
40+
41+
import * as Foo3 from "./exporter.mjs";
42+
>Foo3 : typeof Foo
43+
> : ^^^^^^^^^^
44+
45+
new Foo3();
46+
>new Foo3() : Foo
47+
> : ^^^
48+
>Foo3 : typeof Foo
49+
> : ^^^^^^^^^^
50+
51+
import { Oops } from "./exporter.mjs";
52+
>Oops : any
53+
> : ^^^
54+
55+
=== /exporter.mts ===
56+
export default class Foo {}
57+
>Foo : Foo
58+
> : ^^^
59+
60+
export { Foo as "module.exports" };
61+
>Foo : typeof Foo
62+
> : ^^^^^^^^^^
63+
>"module.exports" : typeof Foo
64+
> : ^^^^^^^^^^
65+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/importer-cjs.cjs(2,5): error TS2351: This expression is not constructable.
2+
Type 'String' has no construct signatures.
3+
/importer-cts.cts(2,5): error TS2351: This expression is not constructable.
4+
Type 'String' has no construct signatures.
5+
/importer-cts.cts(4,8): error TS1259: Module '"/exporter"' can only be default-imported using the 'esModuleInterop' flag
6+
/importer-cts.cts(8,5): error TS2351: This expression is not constructable.
7+
Type 'String' has no construct signatures.
8+
/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?
9+
10+
11+
==== /importer-cjs.cjs (1 errors) ====
12+
const Foo = require("./exporter.mjs");
13+
new Foo();
14+
~~~
15+
!!! error TS2351: This expression is not constructable.
16+
!!! error TS2351: Type 'String' has no construct signatures.
17+
18+
==== /importer-cts.cts (4 errors) ====
19+
import Foo = require("./exporter.mjs");
20+
new Foo();
21+
~~~
22+
!!! error TS2351: This expression is not constructable.
23+
!!! error TS2351: Type 'String' has no construct signatures.
24+
25+
import Foo2 from "./exporter.mjs";
26+
~~~~
27+
!!! error TS1259: Module '"/exporter"' can only be default-imported using the 'esModuleInterop' flag
28+
new Foo2();
29+
30+
import * as Foo3 from "./exporter.mjs";
31+
new Foo3();
32+
~~~~
33+
!!! error TS2351: This expression is not constructable.
34+
!!! error TS2351: Type 'String' has no construct signatures.
35+
36+
import { Oops } from "./exporter.mjs";
37+
~~~~
38+
!!! error TS2614: Module '"./exporter.mjs"' has no exported member 'Oops'. Did you mean to use 'import Oops from "./exporter.mjs"' instead?
39+
40+
==== /exporter.mts (0 errors) ====
41+
export default class Foo {}
42+
const oops = "oops";
43+
export { oops as "module.exports" };
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [tests/cases/conformance/node/esmModuleExports2.ts] ////
2+
3+
=== /importer-cjs.cjs ===
4+
const Foo = require("./exporter.mjs");
5+
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
6+
>require : Symbol(require)
7+
>"./exporter.mjs" : Symbol("/exporter", Decl(exporter.mts, 0, 0))
8+
9+
new Foo();
10+
>Foo : Symbol(Foo, Decl(importer-cjs.cjs, 0, 5))
11+
12+
=== /importer-cts.cts ===
13+
import Foo = require("./exporter.mjs");
14+
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))
15+
16+
new Foo();
17+
>Foo : Symbol(Foo, Decl(importer-cts.cts, 0, 0))
18+
19+
import Foo2 from "./exporter.mjs";
20+
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))
21+
22+
new Foo2();
23+
>Foo2 : Symbol(Foo2, Decl(importer-cts.cts, 3, 6))
24+
25+
import * as Foo3 from "./exporter.mjs";
26+
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))
27+
28+
new Foo3();
29+
>Foo3 : Symbol(Foo3, Decl(importer-cts.cts, 6, 6))
30+
31+
import { Oops } from "./exporter.mjs";
32+
>Oops : Symbol(Oops, Decl(importer-cts.cts, 9, 8))
33+
34+
=== /exporter.mts ===
35+
export default class Foo {}
36+
>Foo : Symbol(Foo, Decl(exporter.mts, 0, 0))
37+
38+
const oops = "oops";
39+
>oops : Symbol(oops, Decl(exporter.mts, 1, 5))
40+
41+
export { oops as "module.exports" };
42+
>oops : Symbol(oops, Decl(exporter.mts, 1, 5))
43+
>"module.exports" : Symbol("module.exports", Decl(exporter.mts, 2, 8))
44+

0 commit comments

Comments
 (0)