Привет. Я работаю в команде, занимающейся улучшением
пользовательского опыта
при работе с деньгами. Front-end мы поставляем npm-пакетами.
В какой-то момент я столкнулся с проблемами, которые привели меня к
использованию
поля exports
в package.json
Проблема #1
Пакеты могут экспортировать функции с одинаковыми названиями, но
делающие разные вещи. Для примера возьмём 2 стейт-менеджера: Reatom
и Effector.
Они экспортируют функцию createStore
. Если попытаться
экспортитировать их из одного пакета (назовём его
vendors
), то получится следующая картина:
// @some/vendors/index.tsexport { createStore } from '@reatom/core';export { createStore } from 'effector';
Налицо конфликт имён. Такой код просто не будет работать.
Этого можно избежать за счёт as
:
// @some/vendors/index.tsexport { createStore as reatomCreateStore } from '@reatom/core';export { createStore as effectorCreateStore } from 'effector';
Выглядит, прямо скажем, паршиво. По-хорошему, каждый реэкспорт
надо писать через as
для поддержания консистентности.
Как по мне, это ухудшает DX.
Я начал искать решение, которое позволит избежать и конфликта имён
и необходимости писать as
. Как бы оно могло выглядеть
Например, вот так:
// @some/vendors/reatom.tsexport { createStore } from 'reatom';
// @some/vendors/effector.tsexport { createStore } from 'effector';
В двух разных файлах мы пишем обычные экспорты и импортируем
нужную реализацию createStore
:
// someFile.tsimport { createStore } from 'vendors/effector';
Проблема #2
Пакет vendors
, скорее всего, содержит не только
стейт-менеджер, а ещё какую-то библиотеку. Например, Runtypes.
Если не использовать exports
, то импорт будет
выглядеть вот так:
// someFile.tsimport { createStore, Dictionary, createEvent, Record } from 'vendors';
Получается какая-то мешанина. На мой взгляд, приятнее было бы читать что-то в стиле:
// someFile.tsimport { createStore, createEvent } from 'vendors/effector';import { Dictionary, Record } from 'vendors/runtypes';
А ещё хорошо бы скрыть имена библиотек. Это может быть полезно при последующих рефакторингах.
// someFile.tsimport { createStore, createEvent } from 'vendors/state';import { Dictionary, Record } from 'vendors/contract';
Решение
Чтобы добиться таких импортов, необходимо обратиться к полю
exports
в package.json
// package.json"exports": { "./contract": "./build/contract.js", "./state": "./build/state.js", "./package.json": "./package.json"}
По сути, мы просто говорим сборщику как резолвить импорты. Если
вы пишете на TypeScript, то это ещё не всё.
В package.json есть поле types
, которое позволяет
указать, где находятся типы пакета. В значении у него строка. Не
получится указать типы и для contract
, и для
state
. Так что же делать?
Тут на помощь приходит typesVersions
в
package.json
// package.json"typesVersions": { "*": { "contract": ["build/contract.d.ts"], "state": ["build/state.d.ts"] }}
Мы делаем то же самое, что и в exports
, но для
d.ts
файлов, получая рабочие типы.
Заключение
Использование exports
не ограничивается проблемой
создания пакета vendors
. Его можно использовать для
улучшения DX.
Например, в Effector базовый импорт выглядит вот так:
import { createEvent } from 'effector';
А для поддержки старых браузеров вот так:
import { createEvent } from 'effector/compat';
Какие ещё проблемы решает exports
можно
ознакомиться
тут.
Посмотреть на репозиторий с примером
здесь.
Спасибо!