Найти в Дзене
IlyaDev

Создание UI-кита для React/TS с избирательным импортом компонентов

Привет друг! В этой статье попробую описать, один из вариантов создания ui-kit для react, с возможностью избирательного импорта компонент. Если вдруг установил в проект uikit, а там ошибка: TS2307: Cannot find module super-ui-kit/ Button or its corresponding type declarations. There are types at */super-ui-kit/dist/components/Button/index. d. ts , but this result could not be resolved under your current moduleResolution setting. Consider updating to node16, nodenext, or bundler Тебе сюда 🥸.
Мотивация: Настроить сборку и экспорт компонентов таким образом, чтобы девелопер мог импортировать только те компоненты, которые ему нужны, и не тащить весь uikit в каждую компоненту. P.S. Подробно останавливаться на каждом шаге, не буду 😇, информации во всемирной паутине, по этому поводу - много, а вот что касаемо нужного мне импорта, как то поскупились🤬. Постараюсь заострить внимание на узких местах. Ну а теперь, поехали!🛼 1.Инициализация проекта: mkdir super-ui-kit && cd super-ui-kit
npm ini
Оглавление

Привет друг! В этой статье попробую описать, один из вариантов создания ui-kit для react, с возможностью избирательного импорта компонент.

Если вдруг установил в проект uikit, а там ошибка:

TS2307: Cannot find module super-ui-kit/ Button or its corresponding type declarations.
There are types at
*/super-ui-kit/dist/components/Button/index. d. ts
, but this result could not be resolved under your current moduleResolution setting. Consider updating to node16, nodenext, or bundler

Тебе сюда 🥸.


Мотивация:
Настроить сборку и экспорт компонентов таким образом, чтобы девелопер мог импортировать только те компоненты, которые ему нужны, и не тащить весь uikit в каждую компоненту.

P.S. Подробно останавливаться на каждом шаге, не буду 😇, информации во всемирной паутине, по этому поводу - много, а вот что касаемо нужного мне импорта, как то поскупились🤬. Постараюсь заострить внимание на узких местах.

Ну а теперь, поехали!🛼

Шаг 1: Настройка проекта:

1.Инициализация проекта:

mkdir super-ui-kit && cd super-ui-kit
npm init -y

2. Установка зависимостей:

npm install react react-dom clsx --save
npm install typescript esbuild rimraf stylelint sass postcss autoprefixer glob --save-dev
npm install @types/react @types/react-dom --save-dev

3. Создание файлов конфигурации (в корне проекта, разумеется):

  • package.json (содержит настройку exports для избирательного импорта).
  • tsconfig.json и tsconfig.build.json (разделение настроек для разработки и сборки).
  • esbuild.config.js (для компиляции компонентов).
  • postbuild.js (перемещение и исправление путей в .d.ts файлах).

Шаг 2: Структура проекта:

super-ui-kit/
├── src/
│ ├── components/
│ │ ├── Button1/
│ │ │ ├── index.ts
│ │ │ ├── Button.module.scss
│ │ │ └── Button.tsx
│ │ ├── Button2/
│ │ │ ├── index.ts
│ │ │ └── Button2.module.scss
│ │ │ └── Button2.tsx
│ ├── index.ts
│ ├── styles/
│ │ └── global.css
├── index.ts
├── global.d.ts
├── esbuild.config.js
├── postbuild.js
├── tsconfig.json
├── tsconfig.build.json
├── package.json
└── postcss.config.js

postcss.config.js:

export default {
plugins: {
autoprefixer: {},
},
};

Шаг 3: Реализация export-ов компонент (index.ts):

uper-ui-kit/
├── src/
│ ├── components/
│ │ ├── Button1/
│ │ │ ├── index.ts (export {Button1, type Button1Props} from './Button1';)
│ │ │ ├── Button.module.scss
│ │ │ └── Button.tsx
│ │ ├── Button2/
│ │ │ ├── index.ts (export {Button2, type Button2Props} from './Button2';)
│ │ │ └── Button2.module.scss
│ │ │ └── Button2.tsx
│ ├── index.ts (export * from './Button1'; export * from './Button2';)
│ ├── styles/
│ │ └── global.css
├── index.ts (export * from './components';)

Тут же опишу global.d.ts, нужен нам для работы с модульными стилями 💅:

declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}

Шаг 4: Сборка проекта

1. Сборка кода (esbuild): В esbuild.config.js настрой компиляцию отдельных компонентов в esm-формате (комментарии оставлю в коде):

import path from 'path';

import esbuild from 'esbuild';
import {
glob } from 'glob';
import stylePlugin from 'esbuild-style-plugin';

// 1. Используем glob для нахождения всех файлов index.ts в папке компонентов.
// Отфильтровываем файлы, заканчивающиеся на .stories.tsx (не включаем тестовые и стори-файлы в сборку).
const componentFiles =
glob
.sync('./src/components/*/index.ts')
.filter((file) => !file.endsWith('.stories.tsx'));

// 2. Функция для сборки отдельного компонента.
// Каждому компоненту присваивается уникальная директория в dist/components/[componentName].
const buildComponent = (entry) => {
const componentName = path.basename(path.dirname(entry)); // Извлекаем имя компонента из пути.
console.log(componentName);
return esbuild.build({
entryPoints: [entry], // Точка входа — index.ts компонента.
bundle: true, // Объединяем все зависимости в один бандл.
format: 'esm', // Используем ESM (ECMAScript Modules) формат.
outdir: `dist/components/${componentName}`, // Директория для сборки компонента.
external: ['react', 'clsx'], // Эти библиотеки исключаем из бандла (peerDependencies).
plugins: [
stylePlugin({
cssModules: true, // Поддержка CSS Modules.
minify: true, // Минификация CSS.
}),
],
});
};

// 3. Функция для сборки всех компонентов и главного индекса.
const buildAll = async () => {
// Параллельная сборка всех компонентов.
await
Promise.all(componentFiles.map(buildComponent));

// Сборка главного entry-файла в формате ESM.
await esbuild.build({
entryPoints: ['./src/index.ts'], // Главный файл библиотеки.
bundle: true,
format: 'esm',
outfile: './dist/index.esm.js', // Выходной файл для ESM.
external: ['react', 'clsx'],
plugins: [
stylePlugin({
cssModules: true,
extract: true, // Извлечение всех CSS в отдельный файл.
minify: true,
}),
],
});

// Сборка главного entry-файла в формате CommonJS (для Node.js).
await esbuild.build({
entryPoints: ['./src/index.ts'],
bundle: true,
format: 'cjs',
outfile: './dist/index.js', // Выходной файл для CJS.
external: ['react', 'clsx'],
plugins: [
stylePlugin({
cssModules: true,
extract: true,
minify: true,
}),
],
});
};

// 4. Выполняем сборку. Если есть ошибка — она выводится в консоль, и процесс завершится с кодом 1.
buildAll().catch((e) => {
console.error(e);
process.exit(1);
});

2. Сборка типов (TypeScript): Выполните tsc с помощью tsconfig.build.json.

{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./dist/types",
"outDir": "./dist",
"moduleResolution": "node",
"module": "ESNext",
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["src/**/*.stories.tsx", "node_modules"] // истключаем наши стори, ну и node_modules
}

3. Обработка после сборки: Перенос .d.ts файлов с помощью postbuild.js (комментарии оставлю в коде).

import fs from 'fs';
import
path from 'path';

import {
glob } from 'glob';

// 1. Директории с типами и компонентами.
const typesDir = './dist/types';
const componentsDir = './dist/components';

// 2. Находим все файлы с типами (.d.ts) в папке dist/types.
const files =
glob.sync(`${typesDir}/**/*.d.ts`);

// 3. Функция для исправления путей внутри файлов .d.ts.
// Преобразует пути относительных импортов в формате TypeScript в формат ESM (добавляя ".js").
const fixPaths = (file) => {
let content = fs.readFileSync(file, 'utf8');
content = content.replace(/(from\s+['"])(\.\.?\/[^'"]+)(['"])/g, '$1$2.js$3');
fs.writeFileSync(file, content, 'utf8'); // Сохраняем изменения.
console.log(`Processed: ${file}`);
};

// 4. Функция для перемещения файла из одной директории в другую.
const moveFile = (src, dest) => {
const destDir = path.dirname(dest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true }); // Создаём директорию, если её нет.
}
fs.renameSync(src, dest); // Перемещаем файл.
console.log(`Moved: ${src} -> ${dest}`);
};

// 5. Обрабатываем каждый файл:
// - Исправляем пути импортов.
// - Перемещаем файл в соответствующую папку компонентов.
files.forEach((file) => {
fixPaths(file);

// Генерируем относительный путь файла, исключая лишние директории.
const relativePath = path.relative(typesDir, file);

const cleanPath = relativePath
.split(path.sep)
.filter((segment, index) => !(index === 0 && segment === 'components')) // Исключаем начальную папку 'components'.
.join(path.sep);

console.log(`>>>> Final Calculated Path: ${cleanPath}`);
const targetPath = path.resolve(componentsDir, cleanPath);

moveFile(file, targetPath);
});

// 6. Удаляем папку dist/types после обработки.
if (fs.existsSync(typesDir)) {
fs.rmSync(typesDir, { recursive: true });
console.log(`Removed: ${typesDir}`);
}

Шаг 5: Настройка экспорта

В package.json настрой exports для избирательного импорта:

"exports": {
".": {
"types": "./dist/components/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./Button1": {
"types": "./dist/components/Button1/index.d.ts",
"import": "./dist/components/Button1/index.js",
"require": "./dist/components/Button1/index.js"
},
"./
Button2": {
"types": "./dist/components/
Button2/index.d.ts",
"import": "./dist/components/
Button2/index.js",
"require": "./dist/components/
Button2/index.js"
},
"./index.css": {
"default": "./dist/index.css"
}
},

Шаг 6: Пишем скрипты:


В package.json прописываем скрипты, если читаешь эту статью, то значит и описывать, происходящие ниже, не имеет смысла. Опять соскочил 😋:

"scripts": {
"clean": "rimraf dist",
"build:code": "node esbuild.config.js",
"build:types": "tsc -p tsconfig.build.json",
"postbuild": "node postbuild.js",
"build": "npm run clean && npm run build:code && npm run build:types && npm run postbuild",
"style-lint": "npx stylelint '**/*.scss' '**/*.css'",
"eslint": "npx eslint .",
"pack-project": "npm run build && del /q *.tgz && npm pack",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},

Жмякаем build - и собранную dist директорию можем размещать где хотим 🥳.

-2

На всякий случай, полный package.json:

{
"name": "
super-ui-kit",
"version": "1.0.0",
"description": "
super-ui-kit",
"main": "./dist/index.esm.js",
"module": "./dist/index.js",
"files": [
"dist/**/*"
],
"types": "./dist/index.d.ts",
"type": "module",
"scripts": {
"clean": "rimraf dist",
"build:code": "node esbuild.config.js",
"build:types": "tsc -p tsconfig.build.json",
"postbuild": "node postbuild.js",
"build": "npm run clean && npm run build:code && npm run build:types && npm run postbuild",
"style-lint": "npx stylelint '**/*.scss' '**/*.css'",
"eslint": "npx eslint .",
"pack-project": "npm run build && del /q *.tgz && npm pack",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"exports": {
".": {
"types": "./dist/components/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./Button1": {
"types": "./dist/components/Button1/index.d.ts",
"import": "./dist/components/Button1/index.js",
"require": "./dist/components/Button1/index.js"
},
"./
Button2": {
"types": "./dist/components/
Button2/index.d.ts",
"import": "./dist/components/
Button2/index.js",
"require": "./dist/components/
Button2/index.js"
},
"./index.css": {
"default": "./dist/index.css"
}
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@chromatic-com/storybook": "^3.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
"@storybook/blocks": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-vite": "^8.4.7",
"@storybook/test": "^8.4.7",
"@types/react": "18.3.17",
"@types/react-dom": "18.3.5",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"autoprefixer": "^10.4.20",
"esbuild": "^0.24.2",
"esbuild-style-plugin": "^1.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react-hooks": "^5.1.0",
"glob": "^11.0.0",
"postcss": "^8.4.49",
"rimraf": "^6.0.1",
"sass": "^1.83.0",
"stylelint": "^16.12.0",
"stylelint-config-standard-scss": "^14.0.0",
"typescript": "^5.7.2"
},
"dependencies": {
"clsx": "^2.0.0",
"react": "^18.0.0 || ^17.0.0",
"react-dom": "^18.0.0 || ^17.0.0"
},
"peerDependencies": {
"clsx": "^2.0.0",
"react": "^18.0.0 || ^17.0.0",
"react-dom": "^18.0.0 || ^17.0.0"
}
}

Шаг 7: Использование UI-кита

После публикации на npm (npm publish) вы можете использовать библиотеку так:

import { Button1 } from 'super-ui-kit/Button1';
import '
super-ui-kit/index.css';

<Button1 label="Click me" onClick={() => console.log('Clicked!')} />;

Ну вот собственно и всё 🚀🚀🚀.

Спасибо за внимание! До Новых Встреч!🤗🤗🤗

-3