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 typeswhich 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!

Similar Posts

Leave a Reply