-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Support package.json exports
outside of Node
#50794
Comments
Incorrect. As I have explained in #33079 (multiple times, with at least 2 attempts of linking a repo, both of which were ignored) package.json exports fields are fully supported by moduleResolution node, they are just broken out of the box. To reiterate the issue: As a library author you want your library to work for as many users as possible. This means it NEEDS to work with ANY resolution mode. Currently resolution mode "node" (the default!) requires the files linked in the "exports": {
"./": {
"types": "./index.d.ts", // works fine, even if it re-exports types!
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./some-module": {
"types": "./dist/types/some-module.d.ts", // does not work but should
"import": "./dist/some-module.js",
"require": "./dist/some-module.cjs"
}
} The resolution mode "node" should be fixed and not renamed. The other two options ("node16" and "nodenext") have actual breaking changes and are not even known to exist to 90% of users. If you are renaming anything, then you should rename "node16" as that is a terrible name that will eventually be outdated and changing it then will cause even more confusion than renaming it to "node-lts" (or "node-latest"? See it's too confusing already, I don't know which it is supposed to be currently) now. Add a "node-legacy" option for people not concerned with security who are sticking with node11 or earlier - the default option should work as a default for 90% of users. EDIT: Here is my current workaround to the broken type import resolution: I literally have to make a new file re-exporting the type declarations generated BY TYPESCRIPT. |
It’s a weird situation to have to assert my credentials on an issue that I authored in a project that it is my full-time job to work on, but here we are? I’ve worked on the TypeScript for the last 3.5 years and have spent nearly every working hour of the last 3 months or so on module resolution. I assure you, there are at most 2–3 people who know as much or more than me about TypeScript’s module resolution as it stands today, and they all have the “Member” badge on this GitHub repo. I’m not willing to debate the validity of the broad claims I’ve made in this issue. Secondly, this issue is about bundlers and non-Node runtimes, so while I appreciate that you were directly responding to my comments about Node, I don’t want to further discuss existing functionality (or naming conventions) intended for Node here. As I said in #33079, if you think you have a bug, please open a new issue. Thank you for understanding. |
Bundlers are exactly the place where the bug I am trying to bring to attention is of concern. Projects that don't use a bundler can simply refactor to node16 resolution, while bundlers often require the "node" module resolution mode and will throw errors when given import paths with file extension, which is what both "node16" and "nodenext" require. |
Right, this issue will be resolved by a new module resolution mode for bundlers, which will include package.json
|
Brief off-topic:
FWIW, I really dislike how this came off. There has been so much confusion around topics of module resolution, so I wanted the information in this issue to be clear, concise, and authoritative, and to be immediately challenged on the fundamental facts underlying it would seem to undermine that goal. I wanted anyone else trying to follow along to rest assured that what I said in the issue body is correct and settled. Working on TypeScript certainly doesn’t make me an authority on everything about it and in most contexts I’m happy to talk through disagreement. The reason I don’t want to debate it here is just that this issue is intended to clarify, and I worked hard to ensure that the narrow set of information I presented is accurate. Attempting to prove my statements would just muddy the waters and be a nuisance for future readers—anyone really interested can validate them by reading |
I'd like to get some help figuring out how to make a library which will work for as many users as possible, that is:
As far as runtime is concerned, there are only two variants of consumers, and the solution is not complicated: just transpile or bundle your code to produce commonjs variant, and use {
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "dist/index.cjs",
} What I'd like to know is how to produce type declarations that will be usable by all 4 variants of the consumers? |
Why is this nested package.json necessary in your example? EDIT: All the following information is now presented in a much more thorough and organized way at https://github.com/andrewbranch/example-subpath-exports-ts-compat. You could stop reading this comment and look through there instead. Definitely the easiest way to have subpath exports that “work” even in module resolvers that don’t support them is to do away with having a "type": "commonjs",
"exports": {
"./subpath": {
"import": "./subpath/index.mjs",
"default": "./subpath/index.js"
},
// ...
}
(Notice that I don’t even have to specify If you must have a dist folder, and/or must put your type definitions in a separate directory, this gets more complicated. The first way is to use directories with package.json files to redirect non-exports-supporting resolvers: // @Filename: package.json
"type": "commonjs",
"exports": {
"./subpath": {
"types": "./types/subpath/index.d.ts",
"import": "./esm/subpath/index.mjs",
"default": "./cjs/subpath/index.js"
},
// ...
} // @Filename: subpath/package.json
{
"main": "../cjs/subpath/index.js",
"types": "../types/subpath/index.d.ts"
} (🚨 Warning: I’m showing a Again, resolvers that support The other way to do this, which only changes the way TypeScript resolves and not any other runtimes or resolvers, is to set up a "type": "commonjs",
"exports": {
"./subpath": {
"types": "./types/subpath/index.d.ts",
"import": "./esm/subpath/index.mjs",
"default": "./cjs/subpath/index.js"
},
},
"typesVersions": {
"*": { // all versions of TypeScript should respect this map
"subpath": ["./types/subpath/index.d.ts"] // syntax is an array
}
} While this approach does not help support Node 11, Parcel, Browserify, etc., one advantage is it supports wildcards. All together: "type": "commonjs",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./esm/index.mjs",
"default": "./cjs/index.js"
},
"./subpath": {
"types": "./types/subpath/index.d.ts",
"import": "./esm/subpath/index.mjs",
"default": "./cjs/subpath/index.js"
},
"./*": {
"types": "./types/*.d.ts",
"import": "./esm/*.mjs",
"default": "./cjs/*.js"
}
},
"typesVersions": {
"*": {
"subpath": ["./types/subpath/index.d.ts"],
"*": ["./types/*.d.ts"]
}
},
"types": "index",
"main": "cjs/index.js" Notice here that the top-level This complexity is why I’m not a fan of In every example I gave here, the library being imported is I gave these examples from memory, so while I’m quite sure the overall ideas are sound, beware that there may still be typos or small errors. I’ll try to port this to a working repo today or tomorrow and fix any errors that turn up in the process. |
Forgive my ignorance: does this example imply that the project source code has both Hopefully this question makes sense. Sorry if it doesn't. |
In talking about the published package contents, I wasn’t necessarily making any assumptions about how it was generated. tsc isn’t designed to help you generate dual ESM and CJS packages from a single source; while it might be possible to do that in some circumstances, it would be really easy to mess it up. tsc will always output a declaration file with an extension that matches the input file, so if a |
First, thanks for the detailed explanations!
Because the package itself can then be
Thanks for the heads up, that's one more argument against default exports I guess.
If all
On the flip side, due to the same error, as soon as you start shipping ESM types, the library becomes unusable by consumers who are I don't know any way around this, it looks like keeping the types
From my limited experience with rollup, it looks like rollup-plugin-typescript could do the job if you run rollup twice with different configs. There's also transform which could be plugged into rollup-plugin-typescript to add |
Yeah, good point. But this is an issue whether the library is using TypeScript / shipping types or not. I would rephrase your sentence to:
This is the core problem library authors are facing with Node. However they choose to solve or work around it, the TypeScript types just follow the implementation. |
As promised: https://github.com/andrewbranch/example-subpath-exports-ts-compat
I didn’t notice any issues with the examples I posted, but the ones in the repo are programmatically validated, more complete, and discussed in more detail. |
- microsoft/TypeScript#50794 - https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c - https://github.com/flex-development/mkbuild Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
- microsoft/TypeScript#50794 - https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
- microsoft/TypeScript#50794 - https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c - https://github.com/flex-development/mkbuild Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
- microsoft/TypeScript#50794 - https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
Is there any eta for this? would be great if the following case with module/nodenext:
... would work.. Because if you create a lib is it great to separate externally exposed paths from the actual internal ones. |
As someone who's in the apparently unusual camp of having a project with So I'd be concerned if broadening the application of Let me know if this is the wrong issue to document this problem and there's a better place for it. With moduleResolution:nodeThis was my proven, reference config. I double-checked via [packages/multiply]$ npx tsc --traceResolution | grep @starter/sum | more
======== Resolving module '@starter/sum' from '/home/cefn/Documents/github/starter/packages/multiply/src/index.ts'. ========
======== Module name '@starter/sum' was successfully resolved to '/home/cefn/Documents/github/starter/packages/sum/src/index.ts'. ======== With moduleResolution:node16 or moduleResolution:nodenextThis effectively introduces a regression to my tooling, by looking at exports, then ignoring the order of default vs types, and settling on the
You can see the reference repo at https://github.com/cefn/starter/tree/8bd9ed9311840e8ca782e13ed6a99d4872f77a02 (a reference commit of the export-typescript-source branch of the repo which demonstrates the issue). The repo is only there to explore this kind of thing, so I'm happy to try workarounds. |
Bundling in this ecosystem just gets more and more confusing. The most relevant summary I could find: microsoft/TypeScript#50794
|
It does support subpath patterns. I’d recommend using |
package.json
exports
are only supported in--moduleResolution node16
and--moduleResolution nodenext
. This is expected behavior, but we are working on expanding it. A few points of common confusion:--moduleResolution node16
. The features added to 16 have fully or mostly been backported to 12 and 14.--moduleResolution node
is for Node 11 and older. Its name is currently bad. We plan to rename it accordingly.node16
andnodenext
are only for Node.*This has been thoroughly discussed elsewhere, so I won’t go into much more detail—I’m creating this just to have an issue to track and point to, since #33079 is closed, but people continue to show up thinking it’s incomplete due to lack of support outside of Node. If you want to participate in discussion about current or future support for package.json
exports
or module resolution outside of Node more generally, it is critical to read #50152 first. Please do not comment here without reading that context.* It’s worth noting that the (intentional) lack of support for package.json
exports
in thenode
(to be renamednode11
soon) mode can be, and has been, worked around for bundler users by many libraries in one of two ways:(EDIT: This is now documented in detail at https://github.com/andrewbranch/example-subpath-exports-ts-compat)
typesVersions
that mirrors the structure ofexports
such that TypeScript’s--moduleResolution node
will reach the same result as TypeScript’s--moduleResolution nodenext
. Note that in this scenario, other resolvers that don’t respectexports
, like Node 11, Parcel, and Browserify, will be broken. The syntax fortypesVersions
is very similar, but not identical, to that ofexports
. If you are a library author who intends to support Webpack, esbuild, Vite, Node 12+, etc., but not Parcel, Browserify, or Node 11, and you need help figuring out how to set uptypesVersions
as an interim workaround, feel free to ask here and I’ll assist.The text was updated successfully, but these errors were encountered: