Exports in package.json
Hello. I work with a user experience improvement team
when dealing with money. We deliver the front-end in npm packages.
At some point, I ran into problems that led me to use
fields exports
in package.json
Problem # 1
Packages can export functions with the same name but do different things. Let’s take 2 state managers as an example: Reatom and Effector.
They export the function createStore
… If you try to export them from one package (let’s call it vendors
), you get the following picture:
// @some/vendors/index.ts
export { createStore } from '@reatom/core';
export { createStore } from 'effector';
There is a conflict of names. This kind of code just won’t work.
This can be avoided by as
:
// @some/vendors/index.ts
export { createStore as reatomCreateStore } from '@reatom/core';
export { createStore as effectorCreateStore } from 'effector';
Looks, frankly, lousy. In an amicable way, each re-export must be written through as
to maintain consistency. This makes DX worse for me.
I started looking for a solution that would avoid both the name conflict and the need to write as
… How could it look … For example, like this:
// @some/vendors/reatom.ts
export { createStore } from 'reatom';
// @some/vendors/effector.ts
export { createStore } from 'effector';
We write regular exports in two different files and import the required implementation. createStore
:
// someFile.ts
import { createStore } from 'vendors/effector';
Problem # 2
Package vendors
, most likely, contains not only the state manager, but also some kind of library. For example, Runtypes.
If not use exports
, then the import will look like this:
// someFile.ts
import { createStore, Dictionary, createEvent, Record } from 'vendors';
It turns out to be some kind of mishmash. In my opinion, it would be nicer to read something in the style:
// someFile.ts
import { createStore, createEvent } from 'vendors/effector';
import { Dictionary, Record } from 'vendors/runtypes';
It would also be nice to hide the library names. This can be useful in subsequent refactorings.
// someFile.ts
import { createStore, createEvent } from 'vendors/state';
import { Dictionary, Record } from 'vendors/contract';
Decision
To achieve such imports, you need to refer to the field exports
in package.json
// package.json
"exports": {
"./contract": "./build/contract.js",
"./state": "./build/state.js",
"./package.json": "./package.json"
}
Basically, we’re just telling the collector how to resolve the imports. If you’re writing in TypeScript, that’s not all.
There is a field in package.json types
which allows you to specify where the package types are located. Its value contains a string. It will not be possible to specify the types for contract
, and for state
… So what should you do?
Here comes to the rescue typesVersions
in package.json
// package.json
"typesVersions": {
"*": {
"contract": ["build/contract.d.ts"],
"state": ["build/state.d.ts"]
}
}
We do the same as in exports
, but for d.ts
files, getting working types.
Conclusion
Using exports
not limited to the problem of creating a package vendors
… It can be used to improve DX.
For example, in Effector, a basic import looks like this:
import { createEvent } from 'effector';
And to support old browsers like this:
import { createEvent } from 'effector/compat';
What other problems does it solve exports
can be found here…
Look at the repository with an example here…
Thank!