DEV Community

Viktor Pasynok
Viktor Pasynok

Posted on

Exports in package.json

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!

Top comments (9)

Collapse
 
artemisprime profile image
Artem Ash

Respectfully, I don't believe you have to do the typesVersion bit as you described. Its not it's purpose:
typescriptlang.org/docs/handbook/d...

I publish all the time wo that and it works fine.

Collapse
 
gpolanco profile image
Geordano Polanco

That's fine if you have a package with 1 file. It's not the real world

Collapse
 
emmiep profile image
Emmie Päivärinta

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

{
  "exports": {
    "./*": "./build/*.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

I haven't tried it myself, but I hope that works for you.

Collapse
 
krutoo profile image
Dmitry Petrov

Thanks but how to provide type declarations with globe?

Collapse
 
opensrc0 profile image
Himanshu Gupta

It will not solve eslint issue. That is a biggest problem I faced.

Collapse
 
aakashgoplani profile image
Aakash Goplani

Can you please explain the need to export "package.json"

"exports": {
  ...
  "./package.json": "./package.json"
},
Enter fullscreen mode Exit fullscreen mode
Collapse
 
oli8 profile image
Olivier

Thanks, the part about Typescript was really useful to me

Collapse
 
opensrc0 profile image
Himanshu Gupta

It will not solve eslint issue. That is a biggest problem I faced.

Collapse
 
fburner profile image
FBurner

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