Repository navigation
Intellisense not resolving modules when using exports in package.json #56412
Description
Activity
RyanCavanaugh commented
on Nov 15, 2023 MemberMore actionsWe need an actual repro. Also, please post code, not screenshots of code.
- addedNeeds More InfoThe issue still hasn't been fully clarifiedThe issue still hasn't been fully clarified
on Nov 15, 2023 I finally got Intellisense to work and respect the
exportsfield.Here is a working repo: https://github.com/magnusriga/my-turborepo
Leaving my config below in case it is helpful to anyone else. The below is slightly different to the repo above, it uses a
srcfolder in package B.Package B's package.json (note the
exportsfield and the omission ofmain){ "name": "@acme/ui", "version": "3.0.1", "private": true, "sideEffects": [ "**/*.css" ], "exports": { ".": { "types": "./types/index.d.ts", "import": "./dist/index.js" }, "./*": { "types": "./types/*.js", "import": "./dist/*.js" } }, "scripts": { "build": "rimraf dist && tsc -p", "clean": "rm -rf dist", "dev": "rimraf dist types .Cache && tsc --watch", "lint": "eslint src/", "test": "jest", "check-types": "tsc --noEmit" }, "jest": { "preset": "jest-presets/jest/browser" }, "devDependencies": { "@babel/preset-react": "latest", "@types/react": "latest", "@types/react-dom": "latest", "@types/styled-components": "latest", "autoprefixer": "latest", "eslint-config-custom": "workspace:*", "fs-extra": "latest", "rimraf": "latest", "postcss": "latest", "react": "latest", "tailwind-config": "workspace:*", "tailwindcss": "latest", "tsconfig": "workspace:*", "typescript": "latest", "watch": "latest" }, "peerDependencies": { "react": "latest", "styled-components": "latest" } }Package A's import
// Intellisense shows only the exported packages when writing the RHS path, which was my goal here import { Button } from '@acme/ui/components/button';
Package B's
tsconfig(the three are chained viaextends){ "extends": "tsconfig/react-library.json", "compilerOptions": { "allowJs": true, "declaration": true, "noEmit": false, "noEmitOnError": true, "incremental": true, "outDir": "dist", "declarationDir": "types", "tsBuildInfoFile": ".Cache/tsc.buildinfo.json", "rootDir": "src", "sourceMap": true, "inlineSources": true, "sourceRoot": "src", "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@components/*": ["src/components/*"], "@utils/*": ["src/utils/*"], "@lib/*": ["src/lib/*"], "@styles/*": ["src/styles/*"], "@ui/*": ["src/ui/*"], "@context/*": ["src/context/*"], "@templates/*": ["src/templates/*"], "@public/*": ["public/*"] } }, "include": ["./src"], "exclude": [ "dist", "build", ".Cache", "types", "node_modules", "cypress", "test", "**/__tests__/*", "jest.setup.tsx" ] }{ "$schema": "https://json.schemastore.org/tsconfig", "display": "React Library", "extends": "./base.json", "compilerOptions": { "jsx": "react-jsx", } }{ "$schema": "https://json.schemastore.org/tsconfig", "display": "Default", "compilerOptions": { "esModuleInterop": true, "skipLibCheck": true, "target": "es2022", "resolveJsonModule": true, "isolatedModules": true, "moduleDetection": "force", "strict": true, "noUncheckedIndexedAccess": true, "moduleResolution": "Bundler", "module": "ESNext", "noEmit": true, "lib": ["es2022", "dom", "dom.iterable"], "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true }, "exclude": ["node_modules"] }andrewbranch commented
on Nov 20, 2023 MemberMore actionsMagnus (@magnusriga) can you please provide instructions on how to reproduce the problem in the context of the repo you shared?
Andrew Branch (@andrewbranch) I actually got it to work. I posted a working repo above to showcase the settings needed to make it work (in case it is useful to others in the future).
Matt Bierner (@mjbvz) can mark it as resolved (I am unable to, since I am no longer the OP).
Reacted by Andrew BranchHey folks, can this be reopened please? Here is a bare-minimum example:
https://github.com/virtuallyunknown/esm-exports-no-intellisense
To reproduce:
- clone the repository
npm install(important step)
The problem can be observed in packages/two/test.js where there is no Intellisense. Why? Because the package we're exporting from -
@repo/onedoes not define amainfield and only uses the modernexportsvariant.Adding a
mainfield fixes the problem.{ "name": "@repo/one", "version": "1.0.0", "author": "me", "type": "module", + "main": "./src/index.js", "exports": { ".": { "import": "./src/index.js" } } }Also, apologies if this is not the correct place to ask. My reason to ask here is because I saw this was redirected from the VSCode repo.
andrewbranch commented
on Dec 11, 2023 MemberMore actionsvirtuallyunknown this is a known issue regarding the default settings that VS Code passes to TS Server (which powers JavaScript IntelliSense). The current defaults are very forgiving, which is good because we don’t know what kind of project a JS user has without additional config, but they lack support for newer features like package.json
"exports". If this is a Node.js project, you can add ajsconfig.jsonfile to your project root like this:{ "compilerOptions": { "module": "nodenext" } }Reacted by François Moreau and Eric MooreReacted by virtuallyunknown, Felipe Barso, Jiří Brabec, Adam Burdette and willy2dgAndrew Branch (@andrewbranch) thanks for taking your time to explain the issue to me, I really appreciate it.
I have tried the
jsconfig.jsonand it seems to do the trick, so I suppose this is an acceptable workaround for the time being (or alternatively using main field), and if you and/or the vscode team are aware of the issue, then that's great.In my personal opinion it's important that developers can make use of all ESM features so we can finally move away from CJS and all start using one unified system, but I also understand you guys probably have more pressing issues that require your attention.
Cheers!
Reacted by Andrew BranchHey Andrew Branch (@andrewbranch) sorry to bother you again, but I have a follow-up question.
In my demo (mono)repository we export some files from
@repo/oneand we import them in@repo/two. Addingjsconfig.jsonin project root fixes the issue with theimportsfield, but another issue arises if@repo/twois a typescript project with atsconfig.jsonof it's own.You can see the basic project overview here, and I updated my demo repository as well with a reproducible example, where the same problem as before arises if
@repo/twois a typescript project.. ├── packages │ ├── one 👉👉👉 export from here │ │ ├── src │ │ │ ├── bar.js │ │ │ ├── foo.js │ │ │ └── index.js │ │ └── package.json │ ├── tsconfig │ │ ├── src │ │ │ ├── tsconfig.node.json │ │ └── package.json │ └── two 👈👈👈 import here │ ├── src │ │ └── dummy.ts │ ├── package.json │ ├── test.js │ └── tsconfig.json ❗❗❗ this is a typescript project now. ├── jsconfig.json ├── package.json └── package-lock.jsonSo I guess my question is if there is a similar workaround for this particular problem? One might think to themselves that this is quite a weird setup to have, but my use case is that I put build scripts in the root of each sub-project (where test.js currently is), and my typescript files reside in the src directory.
If all of this is taking too much of your time then no worries, I will work with what I currently have, cheers.
andrewbranch commented
on Dec 12, 2023 MemberMore actionsWhat’s happening here is the presence of the
tsconfig.jsonnext totest.jsis throwing us off from finding the rootjsconfig.jsonwhen looking for a config to use for editor support. When you opentest.js, we start looking for jsconfig/tsconfig files up the directory spine from that file. We stop when we see the tsconfig.json, but it doesn’t actually include test.js. Instead of continuing the search, it seems like we give up and put test.js in an inferred project with default settings. I don’t know if this is a known limitation or a bug, but it seems like something that could be improved. (cc Sheetal Nandi (@sheetalkamat))I think the most conventional workaround would be to put your scripts in their own directory with their own tsconfig.json/jsconfig.json. Alternatively, you could name your package-level tsconfigs something else (tsconfig.src.json) and reference them from a root-level solution tsconfig.json:
{ "files": [], "references": [ { "path": "./packages/two/tsconfig.src.json" }, { "path": "./tsconfig.scripts.json" } ] }The trick is to make it so that the nearest file named literally
tsconfig.json(orjsconfig.json) up from any file actually includes that file somehow, either directly or by fanning out into a referenced project that includes it.Reacted by virtuallyunknownAndrew Branch (@andrewbranch) Thanks for the awesome support here. I made a change to my repo and now have issues again. If you have time, could you check out my last comment in this PR: vercel/turborepo#6575
Long story short, Turbo's example is a monorepo with, among others, a next.js package and a
@repo/package/ui(only supports TS). The maintainer claims that IntelliSense (auto-import) does not work if we use a catch-all ("./*": "./src/*.tsx") in theexports, due to a TypeScript bug. Is that right?Also, I got it to work if I used a conditional export, any idea why that is? I.e. what is the difference between these:
Works:
"./*": { "import": "./src/*.tsx" }
Does not work:
"./*": "./src/*.tsx"
Lastly, is there a way for IntelliSense to support both TS and JS files? These did not work:
1)
"./*.tsx": { "import": "./src/*.tsx" }, "./*.js": { "import": "./src/*.js" }
2)
"./*": { "import": "./src/*" }
Reacted by BlaineM-SeriouslyRAD and Odd Karstenandrewbranch commented
on Dec 12, 2023 MemberMore actionsAlso, I got it to work if I used a conditional export, any idea why that is? I.e. what is the difference between these
That sounds like a bug. Can you provide a specific repro?
Lastly, is there a way for IntelliSense to support both TS and JS files? These did not work
It’s hard to tell exactly what the intent is here without a repro, but something that seems relevant is file extension substitution. Any time TS sees a request to resolve a file ending in
.js, it actually tries to resolve.ts,.tsx, and.d.tsat a higher priority, because if one of those exists, it’s assumed to be the source of the.jsfile (or typed artifact, in the case of.d.ts).We stop when we see the tsconfig.json, but it doesn’t actually include test.js. Instead of continuing the search, it seems like we give up and put test.js in an inferred project with default settings.
I have actually tried manually including the
test.jsfile in theincludefield of thetsconfig.jsonfile that sits in the same directory next to it, and naively I thought that might do the trick, but unfortunately it didn't.{ "extends": ["@repo/tsconfig/tsconfig.node.json"], "include": ["./src/", "./test.js"], "exclude": ["**/node_modules", "**/.*/"] }Another thing I tested just now is having both jsconfigs and tsconfigs next to one another in a package-level directory, but to no avail either (probably because tsconfig takes precedence as you suggested). The explanation you provided makes sense of what is happening, and one of the workarounds of having the scripts in their own directory with a dedicated
jsconfig.jsonseems to fix the issue. But my use case here is that these plain javascript files are responsible for building/compiling the src directory, so all relative paths would become a mess and at this point all of this is way more trouble than it is worth.I think at this point the best/most sane way to move forward is to keep an
exportsfield for future compatibility and amainfield to keep VSCode/TS Server happy for the time being until hopefully this is fixed or improved in some future release.So yeah, I think I am leaving it like that for the moment, and thanks again for being so helpful, I also learned a thing or two about the way this works which can be useful knowledge in the future :)
andrewbranch commented
on Dec 12, 2023 MemberMore actionsI have actually tried manually including the
test.jsfile in theincludefield of thetsconfig.jsonfile that sits in the same directory next to it, and naively I thought that might do the trick, but unfortunately it didn't.Probably need to add
allowJsorcheckJsto that tsconfig to gettest.jsproperly included.6 remaining items
Can you be more specific about how to repro and what your expected and actual behavior is? This is a big repo with many files, and both “IntelliSense” and “works” are loaded terms 😁
Yes, sorry. Just open the repo I linked in your vscode, then follow these steps:
pnpm i- Go to
apps/web/foo-typescript.jsand see that you get suggestions on the RHS of this statement:import { Bar } from '@repo/ui/'. - Go to
apps/web/foo-javascript.jsand see that you do not get suggestions on the RHS of this statement:import { Card } from '@repo/ui/'.
andrewbranch commented
on Dec 15, 2023 MemberMore actionsMagnus (@magnusriga) the
.jsfile is explicitly excluded from your project. This change makes it work.
Magnus (@magnusriga) the
.jsfile is explicitly excluded from your project. This change makes it work.
Legend Andrew Branch (@andrewbranch) , thank you so much.
Re. my second question above, is it possible to export both JS and TS files with a wild card export, and still be able to see the IntelliSense suggestions when importing?
"./*": "./src/*.tsxworks to export all tsx files while maintaining IntelliSense on import, and so does"./*": "./src/*.jsfor all the js files. However, I want to export both js and ts files and be able to see them all as suggestions by IntelliSense when importing.andrewbranch commented
on Dec 15, 2023 MemberMore actionsAndrew Branch (@andrewbranch) I got the drop-down suggestions to work, following your suggestions. Thank you so much.
What does not work is ctrl-clicking on the right-hand-side of the import (the path) or on the left-hand-side (the module name). It just states:
no definition found
Screenshots:
Do you get the same issue on the repo I shared?
Side question, if you have time:
How come the
tsconfigin thewebfolder (where you addedsrcto theincludeproperty) controls what is seen by Intellisense in other packages outsideweb? Specifically, by addingsrcwe suddenly started seeing js files from external package@repo/ui. I would have thought it was thetsconfiginside@repo/uithat controlled what is seen in that package.@repo/ui/barand@repo/ui/cardare not valid module specifiers via that wildcardexports. The result of a wildcard*substitution is a file lookup, so file extension adding andindexdirectory lookups don't occur. So, the correct module specifiers would be@repo/ui/card.tsxand@repo/ui/bar.js.❯ node Welcome to Node.js v21.3.0. Type ".help" for more information. > require.resolve('@repo/ui/bar') Uncaught: Error: Cannot find module '/Users/andrew/Developer/microsoft/eg/turbo-tailwind/apps/web/node_modules/@repo/ui/src/bar' at createEsmNotFoundErr (node:internal/modules/cjs/loader:1181:15) at finalizeEsmResolution (node:internal/modules/cjs/loader:1169:15) at resolveExports (node:internal/modules/cjs/loader:591:14) at Module._findPath (node:internal/modules/cjs/loader:668:31) at Module._resolveFilename (node:internal/modules/cjs/loader:1130:27) at Function.resolve (node:internal/modules/helpers:187:19) { code: 'MODULE_NOT_FOUND', path: '/Users/andrew/Developer/microsoft/eg/turbo-tailwind/apps/web/node_modules/@repo/ui/package.json' } > require.resolve('@repo/ui/bar.js') '/Users/andrew/Developer/microsoft/eg/turbo-tailwind/packages/ui/src/bar.js'The TS bug is that the path completions suggested
cardandbarwithout file extensions. I'll open a new issue for that.How come the tsconfig in the web folder (where you added src to the include property) controls what is seen by Intellisense in other packages outside web? Specifically, by adding src we suddenly started seeing js files from external package @repo/ui. I would have thought it was the tsconfig inside @repo/ui that controlled what is seen in that package.
TypeScript doesn't know anything about the boundaries of these "packages," because you haven't set up project references. Each package here has its own independent TS project that contains all files you reference, compiled under the single tsconfig for that package. The tsconfigs in other packages are completely independent and never even discovered by the TS project you're editing.
Specifically, by adding src we suddenly started seeing js files from external package @repo/ui
Probably what you were seeing here is that imports in the
apps/web/src/foo-javascript.jswere wrong/incomplete because that file was being processed under default VS Code settings, and once it was included by the intended tsconfig, itsmoduleResolutionsetting changed and made additional import paths available. Changing theincludeofapps/webdid not change what files inpackages/uiare reachable inside theapps/webproject; it just changed what project some of the files inapps/webbelong to, which changed how their imports are processed.@repo/ui/barand@repo/ui/cardare not valid module specifiers via that wildcardexports. The result of a wildcard*substitution is a file lookup, so file extension adding andindexdirectory lookups don't occur. So, the correct module specifiers would be@repo/ui/card.tsxand@repo/ui/bar.js.Got it Andrew Branch (@andrewbranch) . So, there is no way to change the
exportsfield so that the import can be done without explicitly stating the imported module's file extension, if we want to maintain theexportsfield's catch-all form (i.e. avoiding to explicitly list each exported file)?In other words, I only have two options, either state the file extension in the
import, or change theexportsto include the file extension there:OPTION 1
// Package one import { Card } from '@repo/ui/card.tsx';
// Package two "exports": { "./*": "./src/*" }
OPTION 2
// Package one import { Card } from '@repo/ui/card';
// Package two "exports": { "./card": "./src/card.tsx" }
Is it correct that I am limited to these two? If so, which one is better practice?
PS: If it matters, my modules do not need to support CommonJS, only ES.
Thanks!
That's correct, at least according to the
exportsspec written by Node.js, which is what TS tries to follow. Some bundlers may be more flexible than this but there is some ongoing discussion about making those bundler resolvers conform to the spec in future major version bumps. Neither option is better practice than the other. It's up to you.Reacted by MagnusAndrew Branch (@andrewbranch) One other thing I noticed which may also be a bug:
In words, the left-hand-side Intellisense does not work when importing a js file. I.e. the exported function Bar does not show up as a suggestion.
EDIT:
Subpaths in
exportsalso seem to break IntelliSense (auto-complete of RHS import paths does not work):"exports": { "./foo/bar": "./src/bar.js", }
Reacted by Thomas Neger and Daniel PradoOne other thing I noticed which may also be a bug
This is working as intended. JavaScript files imported from
node_modulesare not analyzed for type information by default.Subpaths in exports also seem to break IntelliSense (auto-complete of RHS import paths does not work)
This appears to be a bug.
Reacted by MagnusThis is working as intended. JavaScript files imported from
node_modulesare not analyzed for type information by default.Is there any way to make that work like it does for internal packages, Andrew Branch (@andrewbranch) ? Internal package example with JS import:
PS: The package is indeed from node_modules, but it is our own internally defined package (from within the monorepo).
Yes, I'll try that out and see if there are any objections.
Reacted by MagnusYes, I'll try that out and see if there are any objections.
Not sure if I misunderstood you Andrew Branch (@andrewbranch) .
Did you know of a way to make the left hand side of the import statements work with IntelliSense, when importing a JS file from node_modules (which in reality is from our own monorepo)? If so, how do I do it?
Thanks!
andrewbranch commented
on Jan 10, 2024 MemberMore actionsIf so, how do I do it?
Update to TypeScript 5.4 when it comes out. #56946
Reacted by Magnus, Lyor and Pedro PortoAndrew Branch (@andrewbranch) Quick and somewhat related question, that might or might not be a bug:
In node it is possible to self-reference a package. So, using the repo in this thread, TS and JS modules in
/package/ui, which is named@repo/ui, should be able to import from itself via@repo/ui:import { Foo } from '@repo/ui/components/foo'.The problem I am facing is that I am not getting any autocomplete for my own package when typing the right-hand-side of the import above. Specifically, ctrl+space at
import { Foo } from '@repo/will only list the @repo-packages I have in my package.jsondevDependencyfield.Since node supports self-referencing, it would be great if Intellisense did too.
PS: Quick side-question that is also related. The @-rules in tsconfig's
compilerOptions.paths, how are they resolved? I import package/ui components into the main app, without any bundling, so if those components use @-rules, will those use the tsconfig inpackage/uior in the app the component is imported into?






Reposting from VSCode repo for Magnus (@magnusriga). I transferred the original issue to the wrong repo and now can't transfer it back
Does this issue occur when all extensions are disabled?: Yes
Steps to Reproduce:
exportsfieldmainandtypesfields and see that Intellisense starts working againIntellisense Works When
mainandtypesare set:Package B's
package.jsonPackage A's
import(notice the Intellisense pop-up. It is possible to ctrl-click the path.)Intellisense Does Not Work When only
exportsis set:Package B's
package.json(removedmainandtypes)Package A's
import(not possible to crl+click to find the package, i.e. no Intellisense)