Skip to content

Commit

Permalink
Merge pull request #197 from arethetypeswrong/named-exports
Browse files Browse the repository at this point in the history
Named exports
  • Loading branch information
andrewbranch authored Sep 6, 2024
2 parents d129c2b + 345b1f1 commit 79859b1
Show file tree
Hide file tree
Showing 39 changed files with 2,430 additions and 68 deletions.
6 changes: 6 additions & 0 deletions .changeset/young-donuts-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@arethetypeswrong/core": minor
"@arethetypeswrong/cli": minor
---

New problem kind: **Named exports cannot be detected by Node.js**. Thanks @laverdet!
63 changes: 63 additions & 0 deletions docs/problems/NamedExports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 🕵️ Named ESM exports

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.

## Explanation

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.)

## Consequences

Node.js will crash at startup when accessing the missing exports as named imports:

```ts
import { a } from "./api.cjs";
// SyntaxError: Named export 'a' not found. The requested module './api.cjs' is a CommonJS module,
// which may not support all module.exports as named exports.

import api from "./api.cjs";
api.a; // Ok
```

## Common causes

### Incorrect types

If the types were written by hand, it’s possible that they contain exports that just don’t exist at all in the JavaScript.

### Unanalyzable JavaScript

The static analysis supported by cjs-module-lexer is somewhat limited; for example, this works:

```js
// api.cjs
exports.a = "a";

// main.mjs
import { a } from "./api.cjs";
```

but this does not:

```js
// api.cjs
module.exports = {
a: "a",
};

// main.mjs
import { a } from "./api.cjs";
```

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:

```js
module.exports = {
a: "a", // can't understand this...
};

0 &&
(module.exports = {
a, // but it can understand this, even though it will never run
});
```
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"pnpm": {
"patchedDependencies": {
"[email protected]": "patches/[email protected]"
}
}
}
6 changes: 3 additions & 3 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ particularly ESM-related module resolution issues.`,
.option("--exclude-entrypoints <entrypoints...>", "Specify entrypoints to exclude from checking.")
.option(
"--entrypoints-legacy",
'In packages without the `exports` field, every file is an entry point. Specifying this option ' +
'only takes effect when no entrypoints are automatically detected, or explicitly provided with other options.'
"In packages without the `exports` field, every file is an entry point. Specifying this option " +
"only takes effect when no entrypoints are automatically detected, or explicitly provided with other options.",
)
.addOption(
new Option("--ignore-rules <rules...>", "Specify rules to ignore").choices(Object.values(problemFlags)).default([]),
Expand Down Expand Up @@ -213,7 +213,7 @@ particularly ESM-related module resolution issues.`,
result.problems = groupProblemsByKind(analysis.problems);
}

console.log(JSON.stringify(result));
console.log(JSON.stringify(result, undefined, 2));

if (deleteTgz) {
await unlink(deleteTgz);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/problemUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const problemFlags = {
CJSResolvesToESM: "cjs-resolves-to-esm",
FallbackCondition: "fallback-condition",
CJSOnlyExportsDefault: "cjs-only-exports-default",
NamedExports: "named-exports",
FalseExportDefault: "false-export-default",
MissingExportEquals: "missing-export-equals",
UnexpectedModuleSyntax: "unexpected-module-syntax",
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/render/typed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export async function typed(
const defaultSummary = marked(!emoji ? " No problems found" : " No problems found 🌟");
const summaryTexts = Object.keys(grouped).map((kind) => {
const info = problemKindInfo[kind as core.ProblemKind];
const description = marked(`${info.description} ${info.docsUrl}`);
const description = marked(
`${info.description}${info.details ? ` Use \`--json\` to see ${info.details}.` : ""} ${info.docsUrl}`,
);
return `${emoji ? `${info.emoji} ` : ""}${description}`;
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Build tools:
👺 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
🥴 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
🥴 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
┌─────────────────────────────────────────┬────────┬──────────────────────────────┬──────────────────────────────┬─────────┐
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/snapshots/@[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Build tools:
⚠️ 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
🥴 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
🥴 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
💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/snapshots/@[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Build tools:
🎭 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
🥴 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
🥴 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
┌─────────────────────────────────┬───────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┐
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/test/snapshots/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ postcss v8.4.21
❌ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md
🕵️ 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
┌──────────────────────────────────┬───────────────────────┬───────────────────────┬────────────────────────────┬────────────────────────────┐
│ │ node10 │ node16 (from CJS) │ node16 (from ESM) │ bundler │
Expand Down Expand Up @@ -50,6 +52,7 @@ postcss v8.4.21
│ "postcss/lib/parser" │ ❌ No types │ ❌ No types │ ❌ No types │ ❌ No types │
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤
│ "postcss/lib/postcss" │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │
│ │ │ │ 🕵️ Named exports │ │
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤
│ "postcss/lib/previous-map" │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │ ❓ Missing `export =` │
├──────────────────────────────────┼───────────────────────┼───────────────────────┼────────────────────────────┼────────────────────────────┤
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ $ attw [email protected] --entrypoints . jsx-runtime
vue v3.3.4
🥴 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
🥴 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
🎭 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ $ attw [email protected] --entrypoints vue
vue v3.3.4
🥴 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
🥴 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
┌───────────────────┬──────────────────────────────┐
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ $ attw [email protected] --exclude-entrypoints macros -f ascii
vue v3.3.4
🥴 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
🥴 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
🎭 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ $ attw [email protected] --include-entrypoints foo -f ascii
vue v3.3.4
🥴 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
🥴 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
🎭 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
💀 Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md
🕵️ 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
"vue"
Expand Down Expand Up @@ -89,7 +91,7 @@ bundler: 🟢 (JSON)
node10: 🟢
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
node16 (from ESM): 🕵️ Named exports
bundler: 🟢
***********************************
Expand Down
Loading

0 comments on commit 79859b1

Please sign in to comment.