Skip to content

Commit 79859b1

Browse files
authored
Merge pull request #197 from arethetypeswrong/named-exports
Named exports
2 parents d129c2b + 345b1f1 commit 79859b1

39 files changed

+2430
-68
lines changed

.changeset/young-donuts-heal.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@arethetypeswrong/core": minor
3+
"@arethetypeswrong/cli": minor
4+
---
5+
6+
New problem kind: **Named exports cannot be detected by Node.js**. Thanks @laverdet!

docs/problems/NamedExports.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# 🕵️ Named ESM exports
2+
3+
TypeScript allows ESM named imports of the properties of this CommonJS module, but they will crash at runtime because they don’t exist or can’t be statically detected by Node.js in the JavaScript file.
4+
5+
## Explanation
6+
7+
When you import a CommonJS module in Node.js, the runtime uses [cjs-module-lexer](https://github.com/nodejs/cjs-module-lexer) to determine what properties of the target’s `module.exports` can be accessed with named imports. This problem is detected by running cjs-module-lexer over the JavaScript and comparing the list of exports it finds with the list of (value, non-type-only) exports exposed in the type declaration file. This problem is only issued when the _types_ contain exports not found in the _JavaScript_, not vice versa. (That is, it’s ok for types to be incomplete, but not to declare exports that don’t exist at runtime.)
8+
9+
## Consequences
10+
11+
Node.js will crash at startup when accessing the missing exports as named imports:
12+
13+
```ts
14+
import { a } from "./api.cjs";
15+
// SyntaxError: Named export 'a' not found. The requested module './api.cjs' is a CommonJS module,
16+
// which may not support all module.exports as named exports.
17+
18+
import api from "./api.cjs";
19+
api.a; // Ok
20+
```
21+
22+
## Common causes
23+
24+
### Incorrect types
25+
26+
If the types were written by hand, it’s possible that they contain exports that just don’t exist at all in the JavaScript.
27+
28+
### Unanalyzable JavaScript
29+
30+
The static analysis supported by cjs-module-lexer is somewhat limited; for example, this works:
31+
32+
```js
33+
// api.cjs
34+
exports.a = "a";
35+
36+
// main.mjs
37+
import { a } from "./api.cjs";
38+
```
39+
40+
but this does not:
41+
42+
```js
43+
// api.cjs
44+
module.exports = {
45+
a: "a",
46+
};
47+
48+
// main.mjs
49+
import { a } from "./api.cjs";
50+
```
51+
52+
However, TypeScript has no way of knowing, and no way of indicating in a declaration file, whether CommonJS exports are written in a way that will be statically analyzable. It assumes they _will_ be, and so even a completely correct declaration file for `api.cjs` will indicate that `a` can be imported by name. Since there’s no way to make the types more restrictive without making them incomplete, and since the unalyzable export is an inconvenience for all consumers of the JavaScript, the only solution is to fix the JavaScript. If the JavaScript exports can’t be restructured, it’s possible to “hint” the exports to cjs-module-lexer with an assignment that never executes:
53+
54+
```js
55+
module.exports = {
56+
a: "a", // can't understand this...
57+
};
58+
59+
0 &&
60+
(module.exports = {
61+
a, // but it can understand this, even though it will never run
62+
});
63+
```

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,10 @@
2323
"engines": {
2424
"node": ">=18",
2525
"pnpm": ">=8"
26+
},
27+
"pnpm": {
28+
"patchedDependencies": {
29+
30+
}
2631
}
2732
}

packages/cli/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ particularly ESM-related module resolution issues.`,
7474
.option("--exclude-entrypoints <entrypoints...>", "Specify entrypoints to exclude from checking.")
7575
.option(
7676
"--entrypoints-legacy",
77-
'In packages without the `exports` field, every file is an entry point. Specifying this option ' +
78-
'only takes effect when no entrypoints are automatically detected, or explicitly provided with other options.'
77+
"In packages without the `exports` field, every file is an entry point. Specifying this option " +
78+
"only takes effect when no entrypoints are automatically detected, or explicitly provided with other options.",
7979
)
8080
.addOption(
8181
new Option("--ignore-rules <rules...>", "Specify rules to ignore").choices(Object.values(problemFlags)).default([]),
@@ -213,7 +213,7 @@ particularly ESM-related module resolution issues.`,
213213
result.problems = groupProblemsByKind(analysis.problems);
214214
}
215215

216-
console.log(JSON.stringify(result));
216+
console.log(JSON.stringify(result, undefined, 2));
217217

218218
if (deleteTgz) {
219219
await unlink(deleteTgz);

packages/cli/src/problemUtils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const problemFlags = {
99
CJSResolvesToESM: "cjs-resolves-to-esm",
1010
FallbackCondition: "fallback-condition",
1111
CJSOnlyExportsDefault: "cjs-only-exports-default",
12+
NamedExports: "named-exports",
1213
FalseExportDefault: "false-export-default",
1314
MissingExportEquals: "missing-export-equals",
1415
UnexpectedModuleSyntax: "unexpected-module-syntax",

packages/cli/src/render/typed.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ export async function typed(
4848
const defaultSummary = marked(!emoji ? " No problems found" : " No problems found 🌟");
4949
const summaryTexts = Object.keys(grouped).map((kind) => {
5050
const info = problemKindInfo[kind as core.ProblemKind];
51-
const description = marked(`${info.description} ${info.docsUrl}`);
51+
const description = marked(
52+
`${info.description}${info.details ? ` Use \`--json\` to see ${info.details}.` : ""} ${info.docsUrl}`,
53+
);
5254
return `${emoji ? `${info.emoji} ` : ""}${description}`;
5355
});
5456

packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Build tools:
1212
1313
👺 Import resolved to an ESM type declaration file, but a CommonJS JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md
1414
15-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
15+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1616
1717
1818
┌─────────────────────────────────────────┬────────┬──────────────────────────────┬──────────────────────────────┬─────────┐

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Build tools:
1212
1313
⚠️ A require call resolved to an ESM JavaScript file, which is an error in Node and some bundlers. CommonJS consumers will need to use a dynamic import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSResolvesToESM.md
1414
15-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
15+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1616
1717
💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md
1818

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Build tools:
1313
1414
🎭 Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md
1515
16-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
16+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1717
1818
1919
┌─────────────────────────────────┬───────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┐

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

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ postcss v8.4.21
1414
1515
❌ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md
1616
17+
🕵️ TypeScript allows ESM named imports of the properties of this CommonJS module, but they will crash at runtime because they don’t exist or can’t be statically detected by Node.js in the JavaScript file. Use --json to see the list of exports TypeScript can see but Node.js cannot. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NamedExports.md
18+
1719
1820
┌──────────────────────────────────┬───────────────────────┬───────────────────────┬────────────────────────────┬────────────────────────────┐
1921
│ │ node10 │ node16 (from CJS) │ node16 (from ESM) │ bundler │
@@ -50,6 +52,7 @@ postcss v8.4.21
5052
│ "postcss/lib/parser" │ ❌ No types │ ❌ No types │ ❌ No types │ ❌ No types │
5153
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤
5254
│ "postcss/lib/postcss" │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │
55+
│ │ │ │ 🕵️ Named exports │ │
5356
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤
5457
│ "postcss/lib/previous-map" │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │
5558
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤

packages/cli/test/snapshots/[email protected] --entrypoints . jsx-runtime.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $ attw [email protected] --entrypoints . jsx-runtime
66
77
vue v3.3.4
88
9-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
9+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1010
1111
🎭 Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md
1212

packages/cli/test/snapshots/[email protected] --entrypoints vue.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $ attw [email protected] --entrypoints vue
66
77
vue v3.3.4
88
9-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
9+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1010
1111
1212
┌───────────────────┬──────────────────────────────┐

packages/cli/test/snapshots/[email protected] --exclude-entrypoints macros -f ascii.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $ attw [email protected] --exclude-entrypoints macros -f ascii
66
77
vue v3.3.4
88
9-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
9+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1010
1111
🎭 Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md
1212

packages/cli/test/snapshots/[email protected] --include-entrypoints foo -f ascii.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ $ attw [email protected] --include-entrypoints foo -f ascii
66
77
vue v3.3.4
88
9-
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
9+
🥴 Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. Use --json to see the imports that failed to resolve. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md
1010
1111
🎭 Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md
1212
1313
💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md
1414
15+
🕵️ TypeScript allows ESM named imports of the properties of this CommonJS module, but they will crash at runtime because they don’t exist or can’t be statically detected by Node.js in the JavaScript file. Use --json to see the list of exports TypeScript can see but Node.js cannot. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NamedExports.md
16+
1517
1618
"vue"
1719
@@ -89,7 +91,7 @@ bundler: 🟢 (JSON)
8991
9092
node10: 🟢
9193
node16 (from CJS): 🟢 (CJS)
92-
node16 (from ESM): 🟢 (CJS)
94+
node16 (from ESM): 🕵️ Named exports
9395
bundler: 🟢
9496
9597
***********************************

0 commit comments

Comments
 (0)