Skip to content

Surprising (or incorrect) priorities with package.json exports fields? #46334

Description

I'm trying a scenario of module: nodenext with Vue.js. I hit a few issues with resolution of declaration files.

Here's the current Vue.js declarations package.json:

{
    "name": "vue",
    "version": "3.2.20",
    "description": "The progressive JavaScript framework for buiding modern web UI.",
    "main": "index.js",
    "module": "dist/vue.runtime.esm-bundler.js",
    "types": "dist/vue.d.ts",
    // ...
    "exports": {
      ".": {
        "import": {
          "node": "./index.mjs",
          "default": "./dist/vue.runtime.esm-bundler.js"
        },
        "require": "./index.js"
      },
      // ...
      "./package.json": "./package.json"
    }
    // ...
}

However, referencing this in a project results in the following error:

import * as Vue from "vue";
//                   ~~~~~
// error
// Could not find a declaration file for module 'vue'. 'USER_DIR/hackathon/vue-proj/node_modules/vue/index.js' implicitly has an 'any' type.
//  Try `npm i --save-dev @types/vue` if it exists or add a new declaration (.d.ts) file containing `declare module 'vue';`

This kind of makes sense - I think you could argue that this isn't configured right for moduleResolution: node12 or later.

I was able to get this working by adding

 "exports": {
     ".": {
     "import": {
         "node": "./index.mjs",
         "default": "./dist/vue.runtime.esm-bundler.js"
     },
     "require": "./index.js",
+    "types": "./dist/vue.d.ts"
     },
 }

But the following DID NOT work.

 "exports": {
     ".": {
     "import": {
         "node": "./index.mjs",
         "default": "./dist/vue.runtime.esm-bundler.js"
+        "types": "./dist/vue.d.ts"
     },
     "require": "./index.js",
     },
 }

That part seems like a bug, right?

Activity

  1. changed the title [-]Surprising (or incorrect) priorities with export maps?[/-] [+]Surprising (or incorrect) priorities with package.json export fields?[/+] on Oct 13, 2021
  2. andrewbranch commented on Oct 13, 2021

    @andrewbranch
    Member

    This kind of makes sense - I think you could argue that this isn't configured right for moduleResolution: node12 or later.

    My assumption was that a top-level types should merge with the . of an export map in order to continue offering typing support for the top-level import of all existing libraries that are already using export maps. I logged this at #46281.

  3. weswigham commented on Oct 13, 2021

    @weswigham
    Member

    That part seems like a bug, right?

    Nope. Is the referencing file cjs mode or is esm mode? An import condition is only going to be applicable for an esm mode import - so a cjs mode import (eg, an import in a js file in a package without type:module) isn't going to use the import condition for its imports - it uses the require condition instead.

  4. weswigham commented on Oct 13, 2021

    @weswigham
    Member

    My assumption was that a top-level types should merge with the . of an export map in order to continue offering typing support for the top-level import of all existing libraries that are already using export maps. I logged this at #46281.

    types is a TS-specific main, which exports blocks - I don't see why it shouldn't also block types.

  5. andrewbranch commented on Oct 13, 2021

    @andrewbranch
    Member

    Is the referencing file cjs mode or is esm mode?

    I missed that the package.json didn’t have "type": "module". Daniel Rosenwasser (@DanielRosenwasser) what was the file extension of the importing file?

  6. DanielRosenwasser commented on Oct 14, 2021

    @DanielRosenwasser
    MemberAuthor

    I was importing from a module (.ts file with "type": "module").

  7. DanielRosenwasser commented on Oct 14, 2021

    @DanielRosenwasser
    MemberAuthor

    types is a TS-specific main, which exports blocks - I don't see why it shouldn't also block types.

    I think this is fair, but it highlights 3 things to me

    1. We should really give a more accurate error message

      "A 'types' field was found in this package's 'package.json', but was not used because an 'exports' field was found and took priority over the top-level 'main' and 'types' field."

      Probably needs to be word-smithed, but I think it would be helpful.

    2. We need to be cautious in our messaging - over-eager people will try to use this in regular projects, and existing packages aren't ready to accommodate them.

    3. We probably should help package authors get ready for node12+ resolution modes.

  8. DanielRosenwasser commented on Oct 14, 2021

    @DanielRosenwasser
    MemberAuthor

    In any case, it's a bug that this one didn't work, right?

     "exports": {
         ".": {
         "import": {
             "node": "./index.mjs",
             "default": "./dist/vue.runtime.esm-bundler.js"
    +        "types": "./dist/vue.d.ts"
         },
         "require": "./index.js",
         },
     }
  9. weswigham commented on Oct 14, 2021

    @weswigham
    Member

    default is going to be matched before types, because the default condition is always set, and the object is ordered. (So you probably wanna list the type condition first, since it's not usually set except by TS)

  10. changed the title [-]Surprising (or incorrect) priorities with package.json export fields?[/-] [+]Surprising (or incorrect) priorities with package.json exports fields?[/+] on Oct 15, 2021
  11. DanielRosenwasser commented on Oct 15, 2021

    @DanielRosenwasser
    MemberAuthor

    Oof that's really confusing. Shouldn't import take priority over types too though?

  12. weswigham commented on Oct 15, 2021

    @weswigham
    Member

    The mistake is thinking they're prioritized - they're not. They're either on or off, and the first condition (in object insertion order) that is on is selected.

  13. 11 remaining items

  14. thetutlage commented on Nov 21, 2021

    @thetutlage

    Should this impact the apps not using node12 and no explicit type is defined in package.json file?

    Coz, I have an application that breaks after upgrading to 4.5. Here is a sample repo to reproduce the issue. https://github.com/thetutlage/Typescript-4-5-regression

    Happy to provide more info if required :)

  15. andrewbranch commented on Nov 29, 2021

    @andrewbranch
    Member

    Harminder Virk (@thetutlage) that’s #46770, which I believe is a bug—didn’t realize it was affecting non-node{12,next} resolution modes. Thanks for the repro!

  16. andrewbranch commented on Nov 29, 2021

    @andrewbranch
    Member

    Actually #46770 has a couple different things going on so it’s hard to say what’s what. The issue you reproduced is caused by this:

    const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features: NodeResolutionFeatures.AllFeatures, conditions: ["node", "require", "types"] };

    Type reference directives are always being resolved with NodeResolutionFeatures.AllFeatures, which prevents us from looking at types and main here:

    if (!(state.features & NodeResolutionFeatures.Exports)) {

  17. andrewbranch commented on Dec 3, 2021

    @andrewbranch
    Member

    ^ fixed by #47007

  18. otakustay commented on Jan 6, 2022

    @otakustay

    Also I see nodenext mode do not have a default types resolution, when package.json contains neither types nor exports, but a index.d.ts is placed in package root, no types is resolved, is this intended?

  19. andrewbranch commented on Jan 6, 2022

    @andrewbranch
    Member

    Yes, node12/nodenext has no special handling of index files.

  20. thw0rted commented on Aug 2, 2022

    @thw0rted

    It sounds like the end result of the conversation abve is that if a package has a top level types, but also has a matching exports value with no types, TS will not load any typings (though it could in theory check the matched JS specifier directly when allowJS is set). Is that correct?

    If so, what advice will you give to those who consume packages like this? Declaring types but not exports.{something}.types is basically always an error, right? Is there some mechanism for me to depend on package foo but tell TS to either ignore its exports field, or override it in some way? If not, does that just mean I have to get the library authors to fix their package or make a fork of it myself?

    Also: I don't know if it was brought up previously, but this behavior has a sort of impedance-mismatch with existing frontend bundlers -- webpack respects the exports field today, but bunding TS for browser consumption through webpack means types are pulled from top level types, not the matching exports mapping. Is this issue the right place to address that?

  21. andrewbranch commented on Aug 2, 2022

    @andrewbranch
    Member

    Declaring types but not exports.{something}.types is basically always an error, right?

    First of all, note that if main, or any part of exports points to a JavaScript file, and there is no corresponding types key when resolving through that package.json path, the compiler will look for a declaration file next to that JavaScript file and use that. So a foolproof way to write a valid project structure and package.json file is just to publish your declaration files in the same place as their partner JS files and literally never write types anywhere in your package.json. The only time an author has to start writing types keys is if they break that structure, e.g. by putting JS in one directory and types in another. (This seems to be one of package authors’ absolute favorite ways to make their own lives harder. Maybe it’s the default for some third party tool like rollup?)

    To answer your other questions:

    In node16/nodenext, it is expected for the top-level types to be ignored in the presence of exports, because Node ignores the top-level main in the presence of exports. (The point of a targeted moduleResolution is to maintain a parallel logic like this as perfectly as we can, so there is parity between what works at compile time and what works at runtime.)

    Is there some mechanism for me to depend on package foo but tell TS to either ignore its exports field, or override it in some way?

    Nothing specifically for this problem, but tsconfig paths will probably let you hack something together?

    does that just mean I have to get the library authors to fix their package

    Please 🙏

    but bunding TS for browser consumption through webpack

    My take is that it’s basically a coincidence that TypeScript worked reasonably well with bundlers for years without complaints. Part of the promise of bundlers was that you can develop your frontend projects like Node, using dependencies from npm, and so bundlers copied Node’s module resolution strategy, so --moduleResolution node was appropriate for TS, and then enhanced it in ways that TypeScript could sometimes model with paths or pattern ambient modules and otherwise felt out of scope. But now, Node and bundlers have both diverged away from --moduleResolution node in meaningful ways and different directions. We shipped support for the direction Node went, but we effectively now have no support for what bundlers are doing. This is 90% of what I have been thinking about and working on for the last month. Hoping to publish a proposal this week. So no, this is not the right issue to address that, but there isn’t really a canonical issue for it. Stay tuned.

    Also, I think this issue can be closed?

  22. thw0rted commented on Aug 3, 2022

    @thw0rted

    Excellent answer as always, thanks Andrew. One or two follow-ups though:

    a foolproof way to write a valid project structure and package.json file is just to publish your declaration files in the same place as their partner JS files and literally never write types anywhere in your package.json

    This sounds like a sentence that should appear somewhere on a "for library authors" page in the TS handbook. Does it?

    tsconfig paths will probably let you hack something together? / {"please" get authors to fix their package}

    This isn't a hypothetical. When Webpack started respecting exports, import { ... } from "somelib/assets/file.css" started to throw an "is not exported from package" error. I have an open issue with a pretty popular library that is coming up on its second anniversary (!) while they deliberate how to fix it. At least for the asset, I can work around it by transforming the module specifier into an absolute path using Webpack's resolve.alias. I think you're going to see a lot of new bug reports here in the next couple of months as library authors struggle to migrate to exports and mess up types resolution in the process, if there's not an easy path for consumers to work around the issue.

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

    DiscussionIssues which may not have code impact

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions