Skip to content
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

Tracking issue: Named imports from CJS module incorrectly allowed in nodenext #54018

Open
andrewbranch opened this issue Apr 25, 2023 · 6 comments
Labels
Discussion Issues which may not have code impact

Comments

@andrewbranch
Copy link
Member

This issue has come up a few times, most recently at DefinitelyTyped/DefinitelyTyped#65147 (comment), and I don’t think we’ve had a canonical place to explain, discuss, or track it.

The symptom

Sometimes, when you’re writing an ES module in --module nodenext, you will want to import a CJS dependency, and TypeScript lets you used a named import:

import { createProgram } from "typescript";
createProgram(/* ... */);

but Node complains:

file:///project/out/main.mjs:1
import { createProgram } from "typescript";
         ^^^^^^^^^^^^^
SyntaxError: Named export 'createProgram' not found. The requested module 'typescript' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'typescript';
const { createProgram } = pkg;

Alternatively but equivalently, you might try to use a namespace import:

import * as ts from "typescript";
ts.createProgram(/* ... */);

and Node gives a much less helpful error:

file:///project/out/main.mjs:2
ts.createProgram();
   ^

TypeError: ts.createProgram is not a function

In either case, you find that you instead need to write a default import:

import ts from "typescript";
ts.createProgram(/* ... */);

Both TypeScript and Node are happy with this, but why did TypeScript let you write the forms that crash at runtime?

The problem

Node uses cjs-module-lexer to syntactically analyze CommonJS modules without executing them in order to turn module.exports properties into named exports that can be imported as named (or namespace) imports by ES modules. However, syntactic analysis has its limitations, and not all module.exports properties get detected on all modules. (In practice, it’s common for this to be all-or-nothing, as with the typescript package in its current state: it has no named exports available.)

Type definitions have no way of declaring which exports/properties of a CommonJS module will be detectable by Node’s named export analysis. In other words, typescript.d.ts is not misrepresenting the shape of the module; it would be impossible to “fix” it for Node ESM consumers without breaking it for CJS consumers or bundlers, which typically have more relaxed interop rules. TypeScript currently assumes that all declared exports/properties of a CJS module will be detectable by Node and available as named imports. This is unsound, leading to the error in the example above.

It should be noted that at least for the named import case, the crash occurs during Node’s module linking phase (as soon as the module graph is loaded—at startup, unless the affected part of the graph is isolated in a dynamic import), with a good runtime error message. While annoying to run into, it’s usually immediately diagnosable and fixable. The namespace import variation is a bit more insidious, since the import can be linked, but subsequent property accesses, which can occur later in execution, may fail.

Solutions and non-solutions

I’m putting this issue up for tracking/documentation purposes, not to advocate for a fix. But it’s worth mentioning a few ways of addressing the problem since people will ask or suggest them:

  • We could make a flag that makes potentially unsafe named/namespace imports always error. It would be very cumbersome and catch a lot of false positives, but some people may prefer the safety over the convenience.
  • We could make it possible to annotate modules or individual exports/properties in type declarations that cannot be correctly analyzed by Node. This sounds pretty fraught and fragile to me, given how often people postprocess both their types and JS by third-party build tools.
  • We cannot reasonably determine which exports/properties are detectable by Node by looking at the JS, e.g. by running cjs-module-lexer ourselves, because we never even resolve or read JS sources when types are found for them. Even without incurring the cost of running the (fast) lexer, the performance penalty for doing twice as much module resolution and file system hits would be unacceptable.
@andrewbranch andrewbranch added the Discussion Issues which may not have code impact label Apr 25, 2023
@fatcerberus
Copy link

We could make it possible to annotate modules or individual exports/properties in type declarations that cannot be correctly analyzed by Node. This sounds pretty fraught and fragile to me, given how often people postprocess both their types and JS by third-party build tools.

It also just feels wrong for this to be declared explicitly, seeing as Node’s solution is clearly a best-effort kind of deal (hence the static analysis instead of e.g. side-channel data in package.json), meaning that in a perfect world it would work for all modules; it’s probably not something the person writing the library (or its types) would be consciously thinking about since it’s effectively an implementation detail of Node.

@segevfiner
Copy link

This is quite a common issue in CJS modules, as many are not written with this rule in mind. Even something like lodash.

@andrewbranch
Copy link
Member Author

IMO this merits an issue filed on the library wherever it comes up.

@segevfiner
Copy link

IMO this merits an issue filed on the library wherever it comes up.

Definitely. But not all of them are well maintained. 😢

@jakebailey
Copy link
Member

FWIW as of #57133, the original example from TS no longer fails as I changed how we bundle to "annotate" the exports in such a way that the lexer can see the names, but that obviously doesn't help anyone else.

@shlroland
Copy link

shlroland commented Dec 19, 2024

I encountered a similar problem in issue nodejs/node#56304, where named exports cannot be imported.

Through discussions and analysis with @aduh95, the problem should lie in how cjs-module-lexer handles the cjs files compiled by tsc.Specific information can be found in the content of that issue.

In summary, to solve the problem, either ts or cjs-module-lexer will always need to be modified. Of course, the best scenario is that the cjs module dies right now😄.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests

5 participants