Skip to content

Commit 9cc7b5c

Browse files
committed
Add MissingExportEquals problem
1 parent ad2d97f commit 9cc7b5c

File tree

12 files changed

+1199
-22
lines changed

12 files changed

+1199
-22
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# ❓ Missing `export =`
2+
3+
The JavaScript appears to set both `module.exports` and `module.exports.default` for improved compatibility, but the types only reflect the latter (by using `export default`). This will cause TypeScript under the `node16` module mode to think an extra `.default` property access is required, which will work at runtime but is not necessary. These types `export =` an object with a `default` property instead of using `export default`.
4+
5+
## Explanation
6+
7+
This problem occurs when a CommonJS JavaScript file appears to use a compatibility pattern like:
8+
9+
```js
10+
class Whatever {
11+
/* ... */
12+
}
13+
Whatever.default = Whatever;
14+
module.exports = Whatever;
15+
```
16+
17+
but the corresponding type definitions only reflect the existence of the `module.exports.default` property:
18+
19+
```ts
20+
declare class Whatever {
21+
/* ... */
22+
}
23+
export default Whatever;
24+
```
25+
26+
The types should declare the existence of the `Whatever` class on both `module.exports` and `module.exports.default`. The method of doing this can vary depending on the kinds of things already being exported from the types. When the `export default` exports a class, and that class is the only export in the file, the `default` can be declared as a static property on the class, and the `export default` swapped for `export =`:
27+
28+
```ts
29+
declare class Whatever {
30+
static default: typeof Whatever;
31+
/* ... */
32+
}
33+
export = Whatever;
34+
```
35+
36+
When the file exports additional types, it will be necessary to declare a `namespace` that merges with the class and contains the exported types:
37+
38+
```ts
39+
declare class Whatever {
40+
static default: typeof Whatever;
41+
/* ... */
42+
}
43+
declare namespace Whatever {
44+
export interface WhateverProps {
45+
/* ... */
46+
}
47+
}
48+
export = Whatever;
49+
```
50+
51+
This merging namespace can also be used to declare the `default` property when the main export is a function:
52+
53+
```ts
54+
declare function Whatever(props: Whatever.WhateverProps): void;
55+
declare namespace Whatever {
56+
declare const _default: typeof Whatever;
57+
export { _default as default };
58+
59+
export interface WhateverProps {
60+
/* ... */
61+
}
62+
}
63+
export = Whatever;
64+
```
65+
66+
## Consequences
67+
68+
This problem is similar to the [“Incorrect default export”](./FalseExportDefault.md) problem, but in this case, the types are _incomplete_ rather than wholly incorrect. This incompleteness may lead TypeScript users importing from Node.js ESM code to add an extra `.default` property onto default imports to access the module’s `module.exports.default` property, even though accessing the `module.exports` would have been sufficient.
69+
70+
```ts
71+
import Whatever from "pkg";
72+
Whatever.default(); // Ok, but `Whatever()` would have worked!
73+
```
74+
75+
## Common causes
76+
77+
This problem is usually caused by library authors incorrectly hand-authoring declaration files to match existing JavaScript rather than generating JavaScript and type declarations from TypeScript with `tsc`, or by using a third-party TypeScript emitter that adds an extra compatibility layer to TypeScript written with `export default`. Libraries compiling to CommonJS should generally avoid writing `export default` as input syntax.

packages/core/src/checks/entrypointResolutionProblems.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,35 @@ export function getEntrypointResolutionProblems(
8787
if (
8888
typesExports.has(ts.InternalSymbolName.Default) &&
8989
!typesExports.has(ts.InternalSymbolName.ExportEquals) &&
90-
jsExports.has(ts.InternalSymbolName.ExportEquals) &&
91-
!jsExports.has(ts.InternalSymbolName.Default)
90+
jsExports.has(ts.InternalSymbolName.ExportEquals)
9291
) {
93-
const jsChecker = host
94-
.createProgram([implementationResolution.fileName], {
95-
allowJs: true,
96-
checkJs: true,
97-
})
98-
.getTypeChecker();
99-
// Check for `default` property on `jsModule["export="]`
100-
if (
101-
!jsChecker
102-
.getExportsAndPropertiesOfModule(jsChecker.resolveExternalModuleSymbol(jsSourceFile.symbol))
103-
.some((s) => s.name === "default")
104-
) {
105-
problems.push({
106-
kind: "FalseExportDefault",
107-
entrypoint: subpath,
108-
resolutionKind,
109-
});
92+
if (!jsExports.has(ts.InternalSymbolName.Default)) {
93+
const jsChecker = host
94+
.createProgram([implementationResolution.fileName], {
95+
allowJs: true,
96+
checkJs: true,
97+
})
98+
.getTypeChecker();
99+
// Check for `default` property on `jsModule["export="]`
100+
if (
101+
!jsChecker
102+
.getExportsAndPropertiesOfModule(jsChecker.resolveExternalModuleSymbol(jsSourceFile.symbol))
103+
.some((s) => s.name === "default")
104+
) {
105+
problems.push({
106+
kind: "FalseExportDefault",
107+
entrypoint: subpath,
108+
resolutionKind,
109+
});
110+
return;
111+
}
110112
}
113+
// types have a default, JS has a default and a module.exports =
114+
problems.push({
115+
kind: "MissingExportEquals",
116+
entrypoint: subpath,
117+
resolutionKind,
118+
});
111119
}
112120
}
113121
}

packages/core/src/problems.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ProblemKindInfo {
1818

1919
export const problemKindInfo: Record<ProblemKind, ProblemKindInfo> = {
2020
Wildcard: {
21-
emoji: "",
21+
emoji: "🃏",
2222
title: "Wildcards",
2323
shortDescription: "Unable to check",
2424
description: "Wildcard subpaths cannot yet be analyzed by this tool.",
@@ -89,6 +89,15 @@ export const problemKindInfo: Record<ProblemKind, ProblemKindInfo> = {
8989
docsUrl:
9090
"https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseExportDefault.md",
9191
},
92+
MissingExportEquals: {
93+
emoji: "❓",
94+
title: "Types are missing an `export =`",
95+
shortDescription: "Missing `export =`",
96+
description:
97+
"The JavaScript appears to set both `module.exports` and `module.exports.default` for improved compatibility, but the types only reflect the latter (by using `export default`). This will cause TypeScript under the `node16` module mode to think an extra `.default` property access is required, which will work at runtime but is not necessary. These types `export =` an object with a `default` property instead of using `export default`.",
98+
docsUrl:
99+
"https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/MissingExportEquals.md",
100+
},
92101
UnexpectedModuleSyntax: {
93102
emoji: "🚭",
94103
title: "Syntax is incompatible with detected module kind",

packages/core/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export type EntrypointResolutionProblemKind =
8484
| "CJSResolvesToESM"
8585
| "Wildcard"
8686
| "FallbackCondition"
87-
| "FalseExportDefault";
87+
| "FalseExportDefault"
88+
| "MissingExportEquals";
8889

8990
export interface EntrypointResolutionProblem {
9091
kind: EntrypointResolutionProblemKind;

packages/core/src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export function isEntrypointResolutionProblemKind(kind: ProblemKind): kind is En
102102
case "Wildcard":
103103
case "FallbackCondition":
104104
case "FalseExportDefault":
105+
case "MissingExportEquals":
105106
return true;
106107
default:
107108
return false as AssertNever<typeof kind & EntrypointResolutionProblem["kind"]>;
3.57 KB
Binary file not shown.
14 KB
Binary file not shown.

packages/core/test/snapshots/@[email protected]

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,16 @@
321321
}
322322
},
323323
"problems": [
324+
{
325+
"kind": "MissingExportEquals",
326+
"entrypoint": ".",
327+
"resolutionKind": "node10"
328+
},
329+
{
330+
"kind": "MissingExportEquals",
331+
"entrypoint": ".",
332+
"resolutionKind": "node16-cjs"
333+
},
324334
{
325335
"kind": "FalseCJS",
326336
"entrypoint": ".",

packages/core/test/snapshots/[email protected]

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,5 +500,26 @@
500500
"isWildcard": false
501501
}
502502
},
503-
"problems": []
503+
"problems": [
504+
{
505+
"kind": "MissingExportEquals",
506+
"entrypoint": ".",
507+
"resolutionKind": "node10"
508+
},
509+
{
510+
"kind": "MissingExportEquals",
511+
"entrypoint": ".",
512+
"resolutionKind": "node16-cjs"
513+
},
514+
{
515+
"kind": "MissingExportEquals",
516+
"entrypoint": ".",
517+
"resolutionKind": "node16-esm"
518+
},
519+
{
520+
"kind": "MissingExportEquals",
521+
"entrypoint": ".",
522+
"resolutionKind": "bundler"
523+
}
524+
]
504525
}

0 commit comments

Comments
 (0)