You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cannot import some packages when tsconfig.json specifies "module": "nodenext" #46770
When a TypeScript project specifies "module": "nodenext" (or "node12") in tsconfig.json, tsc emits errors saying it can't find imported modules or that the exports aren't the right types.
I've put together a demo that shows this with the "ip" and "nullthrows" packages. "ip" doesn't ship with TypeScript declarations and needs "@types/ip", while "nullthrows" comes with declarations. The precise errors are included below but in summary, tsc can't find "ip" and it thinks "nullthrows" is not callable despite the default export being a function.
Playing around, when I add "type": "module" to nullthrows's package.json, its error goes away. When I change ip's package.json from "main": "lib/ip" to "main": "lib/ip.js", its issue goes away. Writing this out, these are likely two different issues with the shared symptom of an ESM package not being able to import either of them.
🔎 Search Terms
esm module nodenext
🕗 Version & Regression Information
typescript@4.6.0-dev.20211110
This is the behavior in every version I tried, and I reviewed the FAQ for entries about modules. The issue is present in the 4.5.0 betas as well, though I understand that ESM support via nodenext/node12 is being deferred.
index.ts:1:21 - error TS2307: Cannot find module 'ip' or its corresponding type declarations.
1 import * as ip from 'ip';
~~~~
index.ts:5:10 - error TS2349: This expression is not callable.
Type 'typeof import("/private/tmp/typescript-esm-demo/node_modules/nullthrows/nullthrows")' has no call signatures.
5 return nullthrows(ip.address());
~~~~~~~~~~
Found 2 errors.
changed the title [-]Cannot import some packages that use "export" in their .d.ts files but don't have "type": "module" in package.json when tsconfig.json specifies "module": "nodenext"[/-][+]Cannot import some packages that use "export" in their .d.ts files when tsconfig.json specifies "module": "nodenext"[/+]on Nov 11, 2021
changed the title [-]Cannot import some packages that use "export" in their .d.ts files when tsconfig.json specifies "module": "nodenext"[/-][+]Cannot import some packages when tsconfig.json specifies "module": "nodenext"[/+]on Nov 11, 2021
Ok, so the ip issue is because the types field of the @types/ip package.json is missing a file extension. We started making the published packages include the .d.ts extension quite a while ago, but I guess that package hasn’t been updated since. In ESM module resolution, extensions are required on import paths, and it seems that behavior spilled over into our lookups under the "types" field as well. I’m not sure if that’s intentional or not—Wesley Wigham (@weswigham)? Either way, we may want to consider doing a onetime fixup of all existing DT packages that don’t have an extension in their "types" field (cc Nathan Shively-Sanders (@sandersn)).
The issue with nullthrows looks like a different bug that I’m surprised we haven’t seen before now? Even though you’re using a default import, the type you’re getting is the module type with the default member.
(This whole thing is made more confusing by the fact that the editor glitches back and forth between CJS and ESM mode resolution because of #46373/#46396.)
I'm also seeing this issue with get-stream@6.0.1 (includes types), which uses the index.js lookup behaviour (no main needed). Adding "type": "module"seems to allow the package to be seen, as TypeScript correctly reports that there is no default export.
Module resolution trace
======== Resolving module 'get-stream' from '/Users/jordan/source/git-ext/packages/git/src/cli-helpers.ts'. ========
Explicitly specified module resolution kind: 'Node12'.
File '/users/jordan/source/git-ext/packages/git/src/package.json' does not exist according to earlier cached lookups.
File '/users/jordan/source/git-ext/packages/git/package.json' exists according to earlier cached lookups.
Loading module 'get-stream' from 'node_modules' folder, target file type 'TypeScript'.
Directory '/Users/jordan/source/git-ext/packages/git/src/node_modules' does not exist, skipping all lookups in it.
Found 'package.json' at '/Users/jordan/source/git-ext/packages/git/node_modules/get-stream/package.json'.
'package.json' does not have a 'typesVersions' field.
'package.json' does not have a 'typings' field.
'package.json' does not have a 'types' field.
'package.json' does not have a 'main' field.
Directory '/Users/jordan/source/git-ext/packages/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/jordan/source/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/jordan/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/node_modules' does not exist, skipping all lookups in it.
Directory '/node_modules' does not exist, skipping all lookups in it.
File '/users/jordan/source/git-ext/packages/git/src/package.json' does not exist according to earlier cached lookups.
File '/users/jordan/source/git-ext/packages/git/package.json' exists according to earlier cached lookups.
Loading module 'get-stream' from 'node_modules' folder, target file type 'JavaScript'.
Directory '/Users/jordan/source/git-ext/packages/git/src/node_modules' does not exist, skipping all lookups in it.
File '/Users/jordan/source/git-ext/packages/git/node_modules/get-stream/package.json' exists according to earlier cached lookups.
'package.json' does not have a 'main' field.
Directory '/Users/jordan/source/git-ext/packages/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/jordan/source/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/jordan/node_modules' does not exist, skipping all lookups in it.
Directory '/Users/node_modules' does not exist, skipping all lookups in it.
Directory '/node_modules' does not exist, skipping all lookups in it.
======== Module name 'get-stream' was not resolved. ========
It definitely finds the package, but for some reason is writing it off entirely as a valid resolution.
This check (which is used in a few places) is causing get-stream to be treated as a ESM package in places (state.features & NodeResolutionFeatures.EsmMode is truthy). As a result no attempt to complete the path .../node_modules/get-stream/index is made.
For the purpose of debugging, I added || candidate.search(/get-stream/g) to the ESM checks to bypass guard just for get-stream. When attempting to compile with that change, get-stream was successfully resolved.
I'm not certain what the purpose of state.features is (to carry forward what Node resolution features are in use in the project, or what node resolution features apply to the package), but it looks like it is either wrong or being misused.
I'm not certain what the purpose of state.features is...
This is directly controlled by the set moduleResolution, so the intent is to carry forward what Node resolution features are in use. Given this, its looking like those state.features & NodeResolutionFeatures.EsmMode checks are making a bad assumption about the package being imported when the requested path lacks a file extension in at least once place.
With some more tracing I've isolated the branch causing the issue.
The decision to not include index lookups depends on the package. In this context any kind of node module can be encountered. However loadModuleFromFile also has an extensionless lookup guard that needs to be dealt with.
nullthrows types are wrong. Well, incomplete. They say it's a cjs module which exports a default member which is a function, which is correct, it does, but it also exports that same function as the module itself. It's types should look something more like
As for @types/ip, yeah, that's an issue. We shouldn't be requiring extensions in main (of cjs packages); we're just applying our esm resolution logic at a step where we shouldn't because we reuse resolution machinery (at least when importing cjs packages - extensionless mains actually aren't supported in node for esm packages!). I have a tiny fix for that up at #47893.
Activity
[-]Cannot import some packages that use "export" in their .d.ts files but don't have "type": "module" in package.json when tsconfig.json specifies "module": "nodenext"[/-][+]Cannot import some packages that use "export" in their .d.ts files when tsconfig.json specifies "module": "nodenext"[/+][-]Cannot import some packages that use "export" in their .d.ts files when tsconfig.json specifies "module": "nodenext"[/-][+]Cannot import some packages when tsconfig.json specifies "module": "nodenext"[/+]andrewbranch commentedon Nov 11, 2021
Ok, so the
ipissue is because thetypesfield of the@types/ippackage.json is missing a file extension. We started making the published packages include the.d.tsextension quite a while ago, but I guess that package hasn’t been updated since. In ESM module resolution, extensions are required on import paths, and it seems that behavior spilled over into our lookups under the"types"field as well. I’m not sure if that’s intentional or not—Wesley Wigham (@weswigham)? Either way, we may want to consider doing a onetime fixup of all existing DT packages that don’t have an extension in their"types"field (cc Nathan Shively-Sanders (@sandersn)).The issue with
nullthrowslooks like a different bug that I’m surprised we haven’t seen before now? Even though you’re using a default import, the type you’re getting is the module type with thedefaultmember.(This whole thing is made more confusing by the fact that the editor glitches back and forth between CJS and ESM mode resolution because of #46373/#46396.)
Silic0nS0ldier commentedon Nov 25, 2021
I'm also seeing this issue with
get-stream@6.0.1(includes types), which uses theindex.jslookup behaviour (nomainneeded). Adding"type": "module"seems to allow the package to be seen, as TypeScript correctly reports that there is no default export.Module resolution trace
It definitely finds the package, but for some reason is writing it off entirely as a valid resolution.
Silic0nS0ldier commentedon Nov 25, 2021
I'm not certain, but I think I found the issue.
TypeScript/src/compiler/moduleNameResolver.ts
Lines 1675 to 1676 in 1cee973
This check (which is used in a few places) is causing
get-streamto be treated as a ESM package in places (state.features & NodeResolutionFeatures.EsmModeis truthy). As a result no attempt to complete the path.../node_modules/get-stream/indexis made.For the purpose of debugging, I added
|| candidate.search(/get-stream/g)to the ESM checks to bypass guard just forget-stream. When attempting to compile with that change,get-streamwas successfully resolved.I'm not certain what the purpose of
state.featuresis (to carry forward what Node resolution features are in use in the project, or what node resolution features apply to the package), but it looks like it is either wrong or being misused.Silic0nS0ldier commentedon Nov 25, 2021
This is directly controlled by the set
moduleResolution, so the intent is to carry forward what Node resolution features are in use. Given this, its looking like thosestate.features & NodeResolutionFeatures.EsmModechecks are making a bad assumption about the package being imported when the requested path lacks a file extension in at least once place.With some more tracing I've isolated the branch causing the issue.
TypeScript/src/compiler/moduleNameResolver.ts
Lines 2020 to 2027 in 1cee973
And further
TypeScript/src/compiler/moduleNameResolver.ts
Lines 1675 to 1678 in 1cee973
The decision to not include
indexlookups depends on the package. In this context any kind of node module can be encountered. HoweverloadModuleFromFilealso has an extensionless lookup guard that needs to be dealt with.Silic0nS0ldier commentedon Nov 25, 2021
I don't propose this as a "solution" (excuse the comments everywhere, I didn't clean up before committing) but this does fix the resolution issues for me. https://github.com/Silic0nS0ldier/TypeScript/commit/dd8d8d902dafe4e8a5b76dd9c6cf2cef1bce78dd
Fix: add types to exports field to compat nodenext module resolution
Fix main field to be compatible with nodenext module resolution
Fix main field to be compatible with nodenext module resolution
Fix types field to be compatible with nodenext module resolution
Fix exports field to be compatible with nodenext module resolution
fix: add types to exports field to be compatible with nodenext module…
chore: add types to exports field to be compatible with nodenext modu…
chore: add types to exports field to be compatible with nodenext modu…
chore: add types to exports field to be compatible with nodenext modu…
weswigham commentedon Feb 14, 2022
nullthrowstypes are wrong. Well, incomplete. They say it's a cjs module which exports adefaultmember which is a function, which is correct, it does, but it also exports that same function as the module itself. It's types should look something more likeor something else to that effect.
As for
@types/ip, yeah, that's an issue. We shouldn't be requiring extensions inmain(of cjs packages); we're just applying our esm resolution logic at a step where we shouldn't because we reuse resolution machinery (at least when importing cjs packages - extensionlessmains actually aren't supported innodefor esm packages!). I have a tiny fix for that up at #47893.chore: add types to exports field to be compatible with nodenext modu…
fix: improve compatibility with node16 module resolution (#689)
fix: improve compatibility with node16 module resolution (#711)
fix: improve compatibility with node16 module resolution (#1595)