В этой заметке разберем, как запаковать библиотеку с помощью Rollup, в качестве стека будет React, Typescript и Css Модули. В дальнейшем мы сможем поставлять ее в другие проекты в качестве внешней зависимости, с помощью npm install our-awesome-button
Это будет классическая кнопка, то есть React компонент нашей классной кнопки, просто потому что это первое, что пришло мне в голову. 😁
Создаем пустой проект и инициализируем файл package.json
mkdir our-awesome-button
cd our-awesome-button
npm init -y
Устанавливаем необходимые пакеты
npm install --save-dev typescript react @types/react rollup rollup-plugin-typescript2 rollup-plugin-postcss postcss-modules rollup-plugin-ignore @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-terser @babel/core @babel/preset-env @babel/preset-typescript @babel/preset-react rollup-plugin-babel
Настройка
Начнем с инициализации tsconfig.json
npx tsc --init
Теперь нужно доопределить некоторые специфичные для нашей либы штуки - добавляем jsx, в качестве модулей ES2015, и дополнительно указываем путь к declaration.d.ts
{
"compilerOptions": {
"jsx": "react",
"module": "ES2015",
"typeRoots": ["./node_modules/@types", "./declaration.d.ts"],
"declaration": true, // Включает генерацию d.ts файлов
// ...остальное...
}
}
Мы добавили строчку typeRoots чтобы указать путь к declaration.d.ts
(который мы также должны создать), в этом файле мы объявим css модули, иначе Typescript будет злостно ругаться при сборке со словами Cannot find module './styles.module.css' or its corresponding type declarations.
Немного забежал вперед с ошибками сборки, подробности ниже.
Итак, содержимое declaration.d.ts
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
Конфигурация для rollup
Создаем файл конфигурации для rollup
mkdir rollup.config.js
C таким содержимым
import typescript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import postcssModules from 'postcss-modules';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
const isDev = process.env.NODE_ENV === 'development';
const commonPlugins = [
resolve(),
commonjs(),
typescript(),
babel({
exclude: 'node_modules/**',
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react',
],
}),
];
const cjsPlugins = [
...commonPlugins,
postcss({
extract: true,
plugins: [postcssModules({
generateScopedName: '[local]',
minimize: !isDev,
})],
}),
];
const esmPlugins = [
...commonPlugins,
postcss({
extract: false,
plugins: [postcssModules({
generateScopedName: '[local]',
minimize: !isDev,
})],
}),
];
if (!isDev) {
cjsPlugins.push(terser());
esmPlugins.push(terser());
}
/**
* Rollup configuration
* @type {import('rollup').RollupOptions}
*/
const cjsConfig = {
input: 'src/index.tsx',
output: [
{
file: 'dist/cjs/index.js',
format: 'cjs',
exports: 'named',
sourcemap: true,
},
],
external: ['react'],
plugins: cjsPlugins,
};
/**
* Rollup configuration
* @type {import('rollup').RollupOptions}
*/
const esmConfig = {
input: 'src/index.tsx',
output: [
{
file: 'dist/esm/index.esm.js',
format: 'esm',
exports: 'named',
sourcemap: true,
},
],
external: ['react'],
plugins: esmPlugins,
};
export default [cjsConfig, esmConfig];
На что нужно обратить внимание
-
Строчка
external: ['react']
- говорит о том, что react является внешним модулем и его код не надо включать в банлд, что это значит? Это значит что для esm версии вместо кода react будет строчкаimport react from 'react'
, а в cjs версии -var react = require('react')
. -
generateScopedName: '[local]'
- С другим значением в банлд попадают разные названия классов в стилях и с самом компоненте, Если поменяете на что-то другое и посмотрите на результат сборки - поймете, о чем я говорю. Для меня сработало только значение local, возможно есть лучшее решение. -
За сжатие содержимого отвечает плагин
terser
, за сжатие стилей отвечает строчкаminimize: !isDev
- тут можете настроить на свое усмотрение, можно отключить сжатие, если вам важно сохранить хоть какую-то читаемость кода после сборки, тут, как говорится "Зависит от обстоятельств". В нашем случае все завязано на значение env переменной NODE_ENV -
Last, but not least - мы собираем нашу кнопку в двух вариантах: cjs и esm. Чтобы в зависимости от системы модулей окружения, в котором будет импортироваться кнопка, импортировалось более подходящий модуль. Для esm модулей мы инжектим стили прямо в бандл - Это дает возможность заимпортировать компонент одной строчкой и сразу использовать его (И не напрягаться с отдельным импортом стилей, потому что они уже соберуться в js бандл):
import { Button } from 'our-awesome-button;
Для cjs модулей мы экстрактим стили в отдельный файл (extract: true
) - это делается для того, чтобы оставить возможность заимпортировать стили вручную, но с вероятностью 90% - это не потребуется.
Как и где это будет работать?
Возьмем например nextjs, у него есть серверный и клиентский рендеринг. Для клиентского рендеринга во вмемя сборки подтянется esm модуль с вшитыми в банлд стилями, а во время серверного рендеринга, приложение на nodejs использует require и подключит cjs модуль c с помощью require, на сервере стили не нужны, поэтому мы не включили стили в банлд.
Правки в package.json
Для того, чтобы подключался esm либо cjs модуль нужно рассказать в нашем package.json, что и где у нас лежит, а также не забыть указать пути к типам и исходному коду:
{
"name": "our-awesome-button",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.esm.js",
"types": "dist/esm/index.d.js",
"source": "src/index.tsx",
"files": [
"dist",
"src"
],
"scripts": {
"build:dev": "NODE_ENV=development rollup -c",
"build": "rollup -c",
"build:watch": "rollup -c -w"
},
"peerDependencies": {
"react": "^18.2.0"
},
"devDependencies": {
...
}
}
Итак, в поле main мы указываем путь к cjs бандлу, в поле module - путь к esm бандлу, types отвечает за путь к декларациям типов (они генерируются и для cjs и для esm, разницы, какие именно указать - нет).
Поле source указывает на точку входа в исходники - может быть полезно для настройки сборки для нескольких пакетов в будущем, чтобы не хардкодить пути в rollup.config.js, а считывать их из package.json
Поле files указывает, какие файлы попадут в реджестри при публикации пакета (а также при выполнении команды npm pack) - мы поставляем и dist и src
Подробнее о различных полях в package.json можно почитать в доке npm.
Сам код либы/компонента
Наш компонент кнопки будет очень простым и будет содержать 2 файла в директории src:
- index.tsx
- styles.module.tsx
// index.tsx
import React, { ButtonHTMLAttributes, ReactNode } from 'react';
import styles from './styles.module.css';
export const Button: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
const { children, ...rest } = props;
return <button className={styles.button} {...rest}>{children as ReactNode}</button>;
};
/* styles.module.css */
.button {
background: #4d53b3;
color: #fff;
border-radius: 8px;
border: none;
padding: 10px 8px;
}
Этого будет достаточно, чтобы собрать и проверить работу компонента
Проверяем
npm run build # Собираем пакет (запускаем из корня проекта), проверяем, что папка dist создалась и сборка прошла успешно
pwd # Выводим полный путь и копируем в буффер обмена
cd ~/Projects && npx create-react-app test-cra-app # Создаем новый проект на основе create-react-app
cd test-cra-app && npm install /Path/to/builded/package # Устанавливаем пакет из пути, который выводили в pwd
После того, как собрали проект и установили его в только что созданный проект на create react app, импортируем компонент в src/App.js
// src/App.js
import './App.css';
import { Button } from 'our-awesome-button';
function App() {
return (
<div className="App">
<header className="App-header">
<Button onClick={() => console.log("Hello")}>Test button</Button>
</header>
</div>
);
}
export default App;
Публикация пакета
Тут все просто и много где описан этот процесс (Ссылка на оф. доку):
- Логинитесь в npm с помощью
npm login
- Из корня проекта запускаете
npm publish
(Не забываем перед этим собрать проект с помощью build скрипта) - Ваш пакет в npm! ✨
Пример проекта
Пример проекта, с которого можно стартануть можно найти у меня на гитхабе
В терминале выполняем:
git clone git@github.com:theStrangeAdventurer/rollup-lib-example.git package-dir
cd package-dir
npm i
npm run build