Несколько месяцев назад мы дорабатывали UI-Kit. Стояла задача добавить новое состояние кнопки, при котором кнопка становится неактивной для нажатия и её содержимое меняется на анимированный спиннер, то есть "состояние загрузки".
На реализацию было потрачено несколько часов: взяли компонент из библиотеки, который отвечает за анимированный спиннер, подключили в компонент кнопки и добавили условие изменения содержимого.
Все шло очень гладко, где-то должен был быть подвох. Сборка библиотеки завершилась ошибкой. Причина - текущая инфраструктура библиотеки настроена так, что не позволяет создавать вложенные компоненты.
Инфраструктура построена на следующих инструментах:
- TypeScript@4
- React@17
- Webpack@5
- PostCSS@8
- Storybook/react@6
- ESLint@8, StyleLint@13
- Jest@28
- Lerna@3
Согласно стратегии: монорепозиторий с возможностью публикации каждого компонента отдельно.
TypeScript, Webpack и Jest ожидают, что вложенный компонент будет собран
Typescript
// /ui-kit/packages/button/src/component.tsx
import { Loader } from '@my-company/loader';
При обработке этой строки, TS пытается понять к чему относится импорт в соотвествии с процессом Module Resolution и завершается ошибкой.
error TS2307: Cannot find module '@my-company/loader'.
В моем случае она происходит, потому что в package.json, в поле types, указан путь до файла, который будет создан после сборки компонента.
Самым быстрым решением будет импортировать спиннер из исходного кода.
// /ui-kit/packages/button/src/component.tsx
import { Loader } from '@my-company/loader/src';
Но в этом случае его необходимо будет добавить в итоговый бандл кнопки, что плохо:
- дублирование кода после сборки
- игнорирование особенностей сборки спиннера, его tsconfig.json
На помощь приходит фича Project References, которая доступна с версии TS 3.0. Принцип действия: сразу обратиться к файлу декларации импортируемого модуля и пересобрать его при необходимости.
Webpack
Аналогично TypeScript, у Webpack есть свой процесс Module Resolution, который завершается ошибкой.
Решение заключается в использовании фичи Resolve. Принцип действия: определить свой процесс импортирования модулей.
// /ui-kit/config/webpack.config.js
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
'@my-company/loader': path.join(process.cwd(), '../loader/src'),
},
},
};
Мне не понравилось, что я определил как работать с этими компонентами в двух инструментах. Хочу в одном. К счастью есть TsconfigPathsPlugin - это позволило заменить alias на tsconfig#paths.
ts-loader
Мы используем ts-loader для обработки TypeScript файлов вместо babel-loader, потому что рассматриваем процесс проверки типов и компиляции как единый процесс.
TS6059: File '/ui-kit/packages/loader/src/index.tsx' is not under 'rootDir' '/ui-kit/packages/button/src'. 'rootDir' is expected to contain all source files.
Косвенно это может значить, что Project References не работают.
Я ожидал, что для ts-loader достаточно конфигурации TypeScript, ведь там указаны подключаемые пути. Был прав, но не полностью: конфигурации достаточно, но этот режим работает только если включить его вручную.
Jest
Мы используем ts-jest для обработки TypeScript файлов вместо @babel/preset-typescript потому что при запуске тестов нам нужна проверка типов.
Jest не выбивается из общего поведения стаи и тоже не находит модуль. Во фронтенде каждый делает свой "велосипед", но спасибо, что он едет по общим принципам.
На помощь приходит фича Paths Mapping. Принцип действия: определить свой процесс проверки импортированных модулей.
const jestConfigBase = require('../../config/jest.config.base');
module.exports = {
...jestConfigBase,
moduleNameMapper: {
...jestConfigBase.moduleNameMapper,
'@my-company/loader': '<rootDir>/../loader/src',
},
};
По аналогии с Webpack, есть возможность обратиться к tsconfig#paths.
Правда все же ts-jest работает с Project References не на полную, те игнорирует вложенный tsconfig.json. Что выливается в проблему обработки файлов, с которыми не известно как работать в текущем компоненте:
error TS2307: Cannot find module './assets/loader.svg' or its corresponding type declarations.
Хорошего решения найти не удалось. Временно остановился на том, чтобы только для тестов импортировать файл деклараций из исходного кода спиннера:
// /ui-kit/packages/button/tsconfig.test.json
{
"extends": "../../config/tsconfig.base.json",
...
"include": [
...
"./@test-types"
],
...
}
// /ui-kit/packages/button/@test-types/index.d.ts
import '@my-company/loader/src/custom';
Не понятно как поставлять вложенную стилизацию компонент
Со стилями пока полный провал.
Мы стараемся не отходить от возможностей стандарта CSS (включая черновики), поэтому используем только PostCSS.
После того как количество компонент в библиотеке выросло появилось представление о том, что используется очень часто и какова вариативность при использовании. Общий код обернули в миксины и вынесли в отдельно подключаемый NPM-пакет.
Но, оставить правила @import после сборки нет возможности, потому что при сборке не будет найдено правило объявления миксина. А чтобы не раздувать вес бандла приложения, в которое подключается компонент, во время сборки происходит вынос стилизации в отдельный CSS файл.
По итогу, сейчас, стилизация спиннера дублируется в кнопке. В принципе ничего страшного, тк сборщик приложения может избавиться от дубликатов.
Все же очется удобства разработки: импортнул логику компонента , а стилизация сама подтянулась. Но не забываем и о раздувании бандла. Пока думаем над тем куда двигаться: в React@17 есть поддержка CSS Modules из коробки, в целом должно решить проблему, но и CSS-in-JS подход может быть удобнее при сопоставимых затратах ресурсов.