Hi there! I'm a front-end developer and ship my code via npm-packages.
Once upon a time, I faced problems that led me to the usage of exports
field in package.json
Problem #1
Packages may export functions with the same names but doing different things.
Let's look at 2 state managers: Reatom and Effector. Both of them have a function called createStore
. If we try to export it from the one package (name it vendors
) we'll get this:
// @some/vendors/index.ts
export { createStore } from '@reatom/core';
export { createStore } from 'effector';
We're faced with a name conflict. This code doesn't work. We can repair it with an as
syntax:
// @some/vendors/index.ts
export { createStore as reatomCreateStore } from '@reatom/core';
export { createStore as effectorCreateStore } from 'effector';
Not that pretty? Yeah, It kills DX.
On the other hand, I propose to avoid the necessity of writing as
every time and resolve name conflicts. Here is an example:
// @some/vendors/reatom.ts
export { createStore } from 'reatom';
// @some/vendors/effector.ts
export { createStore } from 'effector';
In 2 different files we write exports as usual and then import needed realization of createStore
:
// someFile.ts
import { createStore } from 'vendors/effector';
Problem #2
Most likely vendors
package contains not only a state manager. It could contain another one lib. Runtypes, for example.
Without using exports
for vendors
imports will look like:
// someFile.ts
import { createStore, Dictionary, createEvent, Record } from 'vendors';
It looks mixed. In my opinion, it will be better to write something like:
// someFile.ts
import { createStore, createEvent } from 'vendors/effector';
import { Dictionary, Record } from 'vendors/runtypes';
It would be nice to encapsulate the names of libraries. It could be useful for refactoring.
// someFile.ts
import { createStore, createEvent } from 'vendors/state';
import { Dictionary, Record } from 'vendors/contract';
Solution
exports
field in package.json
helps us to achieve our goal.
// package.json
"exports": {
"./contract": "./build/contract.js",
"./state": "./build/state.js",
"./package.json": "./package.json"
},
We just say to bundler how to resolve imports.
But if you use TypeScript you need to do one more thing.
There is a field named types
in package.json
. It allows us to specify the location of package types.
Unfortunately, the type of types
is a string. We can't specify types for both contract
and state
. What should we do?
Field typesVersions
resolves this problem.
// package.json
"typesVersions": {
"*": {
"contract": ["build/contract.d.ts"],
"state": ["build/state.d.ts"]
}
},
We do the same thing as for js
files but for d.ts
. And make types working.
Conclusion
Of course, the goal of exports
not only a creation vendors
packages. It could help us to improve DX.
For example, base import from Effector looks like:
import { createEvent } from 'effector';
For supporting old browsers it looks like:
import { createEvent } from 'effector/compat';
What else kind of problems exports
resolves? You can see here.
Also, you can see the repository with an example here.
Thanks!
Top comments (6)
Can you please explain the need to export "package.json"
That's fine if you have a package with 1 file. It's not the real world
Do you mean having to maintain the "exports" field with a large number of exported files? It seems like you can use globbing for these fields, so you should be able to do something like
I haven't tried it myself, but I hope that works for you.
Thanks but how to provide type declarations with globe?
Thanks, the part about Typescript was really useful to me
Im not sure but that doesn work for me
" No "exports" main defined in"
i dont understand also how exports really should work how can i use index.ts localy and index.js in production