Привет друг! В этой статье попробую описать, один из вариантов создания 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 директорию можем размещать где хотим 🥳.
На всякий случай, полный 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!')} />;
Ну вот собственно и всё 🚀🚀🚀.
Спасибо за внимание! До Новых Встреч!🤗🤗🤗