Skip to content

Named exports #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading