Exports in package.json

Viktor Pasynok - Mar 6 '21 - - Dev Community

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode
// @some/vendors/effector.ts

export { createStore } from 'effector';
Enter fullscreen mode Exit fullscreen mode

In 2 different files we write exports as usual and then import needed realization of createStore:

// someFile.ts

import { createStore } from 'vendors/effector';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

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"]
  }
},
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

For supporting old browsers it looks like:

import { createEvent } from 'effector/compat';
Enter fullscreen mode Exit fullscreen mode

What else kind of problems exports resolves? You can see here.
Also, you can see the repository with an example here.

Thanks!

. . .