Skip to content

Intellisense not resolving modules when using exports in package.json #56412

Description

@mjbvz

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

  • VS Code Version: 1.84.2
  • OS Version: Windows 11 Pro

Steps to Reproduce:

  1. Build any project (A) with a dependency on another project (B), where project B's package.json exports files using the exports field
  2. Observer how Intellisense does not work
  3. Insert the main and types fields and see that Intellisense starts working again

Intellisense Works When main and types are set:

Package B's package.json

image

Package A's import (notice the Intellisense pop-up. It is possible to ctrl-click the path.)

image

Intellisense Does Not Work When only exports is set:

Package B's package.json (removed main and types)

image

Package A's import (not possible to crl+click to find the package, i.e. no Intellisense)

image

Activity

  1. RyanCavanaugh commented on Nov 15, 2023

    @RyanCavanaugh
    Member

    We need an actual repro. Also, please post code, not screenshots of code.

  2. magnusriga commented on Nov 17, 2023

    @magnusriga

    I finally got Intellisense to work and respect the exports field.

    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 src folder in package B.

    Package B's package.json (note the exports field and the omission of main)

    {
      "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 via extends)

    {
      "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"]
    }
  3. andrewbranch commented on Nov 20, 2023

    @andrewbranch
    Member

    Magnus (@magnusriga) can you please provide instructions on how to reproduce the problem in the context of the repo you shared?

  4. magnusriga commented on Nov 23, 2023

    @magnusriga

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

  5. virtuallyunknown commented on Dec 11, 2023

    @virtuallyunknown

    Hey folks, can this be reopened please? Here is a bare-minimum example:

    https://github.com/virtuallyunknown/esm-exports-no-intellisense

    To reproduce:

    1. clone the repository
    2. 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/one does not define a main field and only uses the modern exports variant.

    Adding a main field 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.

  6. andrewbranch commented on Dec 11, 2023

    @andrewbranch
    Member

    virtuallyunknown 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 a jsconfig.json file to your project root like this:

    {
      "compilerOptions": {
        "module": "nodenext"
      }
    }
  7. virtuallyunknown commented on Dec 12, 2023

    @virtuallyunknown

    Andrew Branch (@andrewbranch) thanks for taking your time to explain the issue to me, I really appreciate it.

    I have tried the jsconfig.json and 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!

  8. virtuallyunknown commented on Dec 12, 2023

    @virtuallyunknown

    Hey 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/one and we import them in @repo/two. Adding jsconfig.json in project root fixes the issue with the imports field, but another issue arises if @repo/two is a typescript project with a tsconfig.json of 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/two is 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.json
    

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

  9. andrewbranch commented on Dec 12, 2023

    @andrewbranch
    Member

    What’s happening here is the presence of the tsconfig.json next to test.js is throwing us off from finding the root jsconfig.json when looking for a config to use for editor support. When you open test.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 (or jsconfig.json) up from any file actually includes that file somehow, either directly or by fanning out into a referenced project that includes it.

  10. magnusriga commented on Dec 12, 2023

    @magnusriga

    Andrew 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 the exports, 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/*"
      }
  11. andrewbranch commented on Dec 12, 2023

    @andrewbranch
    Member

    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

    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.ts at a higher priority, because if one of those exists, it’s assumed to be the source of the .js file (or typed artifact, in the case of .d.ts).

  12. virtuallyunknown commented on Dec 12, 2023

    @virtuallyunknown

    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.js file in the include field of the tsconfig.json file 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.json seems 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 exports field for future compatibility and a main field 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 :)

  13. andrewbranch commented on Dec 12, 2023

    @andrewbranch
    Member

    I have actually tried manually including the test.js file in the include field of the tsconfig.json file 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 allowJs or checkJs to that tsconfig to get test.js properly included.

  14. 6 remaining items

  15. magnusriga commented on Dec 15, 2023

    @magnusriga

    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:

    1. pnpm i
    2. Go to apps/web/foo-typescript.js and see that you get suggestions on the RHS of this statement: import { Bar } from '@repo/ui/'.
    3. Go to apps/web/foo-javascript.js and see that you do not get suggestions on the RHS of this statement: import { Card } from '@repo/ui/'.
  16. andrewbranch commented on Dec 15, 2023

    @andrewbranch
    Member

    Magnus (@magnusriga) the .js file is explicitly excluded from your project. This change makes it work.

    image
  17. magnusriga commented on Dec 15, 2023

    @magnusriga

    Magnus (@magnusriga) the .js file is explicitly excluded from your project. This change makes it work.

    image

    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/*.tsx works to export all tsx files while maintaining IntelliSense on import, and so does "./*": "./src/*.js for 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.

  18. andrewbranch commented on Dec 15, 2023

    @andrewbranch
    Member

    I can’t repro that issue. With these exports:

      "exports": {
        "./styles.css": "./dist/index.css",
        "./*": "./src/*"
      }
    

    I see:

    image
  19. magnusriga commented on Jan 2, 2024

    @magnusriga

    Andrew 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:

    image

    image

    Do you get the same issue on the repo I shared?


    Side question, if you have time:

    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.

  20. andrewbranch commented on Jan 2, 2024

    @andrewbranch
    Member

    @repo/ui/bar and @repo/ui/card are not valid module specifiers via that wildcard exports. The result of a wildcard * substitution is a file lookup, so file extension adding and index directory lookups don't occur. So, the correct module specifiers would be @repo/ui/card.tsx and @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 card and bar without 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.js were wrong/incomplete because that file was being processed under default VS Code settings, and once it was included by the intended tsconfig, its moduleResolution setting changed and made additional import paths available. Changing the include of apps/web did not change what files in packages/ui are reachable inside the apps/web project; it just changed what project some of the files in apps/web belong to, which changed how their imports are processed.

  21. magnusriga commented on Jan 3, 2024

    @magnusriga

    @repo/ui/bar and @repo/ui/card are not valid module specifiers via that wildcard exports. The result of a wildcard * substitution is a file lookup, so file extension adding and index directory lookups don't occur. So, the correct module specifiers would be @repo/ui/card.tsx and @repo/ui/bar.js.

    Got it Andrew Branch (@andrewbranch) . So, there is no way to change the exports field so that the import can be done without explicitly stating the imported module's file extension, if we want to maintain the exports field'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 the exports to 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!

  22. andrewbranch commented on Jan 3, 2024

    @andrewbranch
    Member

    That's correct, at least according to the exports spec 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.

  23. magnusriga commented on Jan 3, 2024

    @magnusriga

    Andrew Branch (@andrewbranch) One other thing I noticed which may also be a bug:

    image

    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 exports also seem to break IntelliSense (auto-complete of RHS import paths does not work):

    "exports": {
        "./foo/bar": "./src/bar.js",
      }

    image

  24. andrewbranch commented on Jan 3, 2024

    @andrewbranch
    Member

    One other thing I noticed which may also be a bug

    This is working as intended. JavaScript files imported from node_modules are 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.

  25. magnusriga commented on Jan 3, 2024

    @magnusriga

    This is working as intended. JavaScript files imported from node_modules are 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:

    image

    PS: The package is indeed from node_modules, but it is our own internally defined package (from within the monorepo).

  26. andrewbranch commented on Jan 3, 2024

    @andrewbranch
    Member

    Yes, I'll try that out and see if there are any objections.

  27. magnusriga commented on Jan 4, 2024

    @magnusriga

    Yes, 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!

  28. andrewbranch commented on Jan 10, 2024

    @andrewbranch
    Member

    If so, how do I do it?

    Update to TypeScript 5.4 when it comes out. #56946

  29. magnusriga commented on Jan 28, 2024

    @magnusriga

    Andrew 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.json devDependency field.

    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 in package/ui or in the app the component is imported into?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarified

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions