Создание библиотеки на TypeScript
Данная заметка — не полноценное руководство по разработке библиотеки, а лишь краткий конспект с описанием ключевых моментов, каждый из которых можно прогуглить и получить по нему дополнительную информацию. Некоторые детали (вроде полного конфига для webpack’а) вы не найдете тут вовсе, т. к. эти темы уже достаточно изжеваны в интернетах. Кроме того, опустив нюансы, касающиеся типизации, с помощью данной заметки вы сможете создать библиотеку и на JavaScript.
Прежде чем начать описывать детали, я сформулирую ожидаемый результат:
- Код библиотеки должен храниться в приватном репозитории и устанавливаться как пакет npm.
- Библиотека должна версионироваться.
- Должна быть возможность использования библиотеки как в TypeScript-проектах, так и в проектах на JavaScript.
Задачи поставлены, можно действовать.
Зависимости #
В package.json
есть три секции, в которых описываются зависимости проекта.
При разработке библиотеки очень важно понимать разницу между этими секциями:
- dependencies — зависимости, необходимые в production-режиме. Поэтому эти зависимости будут установлены даже когда вы устанавливаете пакет в качестве зависимости.
- devDependencies — зависимости, необходимые для разработки. Когда вы устанавливаете данный пакет в качестве зависимости, зависимости из данной секции установлены не будут. А вот когда вы работаете над самой библиотекой — эти зависимости будут установлены.
- peerDependencies — зависимости, без которых библиотека работать не будет (данные зависимости должны быть установлены у родительского проекта). Пакеты из данной секции вообще не должны попадать в production-сборку (о том как это сделать мы поговорим ниже, когда будем обсуждать настройки webpack’а).
Пример распределения зависимостей:
- dependencies: react-select
- devDependencies: react, react-dom, lodash
- peerDependencies: react, react-dom, lodash
Данное распределение зависимостей говорит о том, что в режиме разработки react, react-dom, react-select и lodash попадут в бандл. При использовании библиотеки в качестве зависимости будет установлен только react-select и только он попадет в бандл. Остальные из указанных зависимостей возьмутся из node_modules
родительского проекта.
Структура каталога src #
В каталоге src я создал 2 директории: lib (код самой библиотеки) и demo (песочница для тестирования и отладки функционала).
Сборка #
Необходимо создать 2 конфига для weback’а:
Development-сборка:
- Сборка dev-бандла (src/demo/) со всеми зависимостями
- Source-maps
- Настройки webpack-dev-server
- Остальное по вкусу
Production-сборка:
- Сборка production-бандла (/src/lib/)
- Перечислите библиотеки из peerDependencies в ключе externals
- Включите у ts-loader (awesome-typescript-loader) флаг transpileOnly, т. к. вы же не собираетесь проверять типы во время сборки библиотеки на компьютере пользователя? 😉
- Заполните output.library (название вашей библиотеки) и output.libraryTarget (umd)
Пропишите в секции scripts вашего package.json
скрипты:
{
"scripts": {
"start": "webpack-dev-server --config [путь к dev-конфигу]",
"build": "webpack --config [путь к production-конфигу]",
"postinstall": "npm run build"
}
}
Скрипт postinstall запустится после установки вашей библиотеки в качестве зависимости. Подробнее можно прочитать на сайте npm.
Alias’ы путей #
Я считаю, что относительные пути при импорте модулей — не круто, т. к. их одинаково тяжело и читать и импортировать новые модули.
import '../../../components/foo' // Плохо
import '@libName/components/foo' // Хорошо
Создать alias очень просто. Это делается в двух местах:
tsconfig.json
{
"compilerOptions": {
"paths": {
"@libName/*": ["src/lib/*"]
}
}
}
Конфиг webpack’а
{
resolve: {
alias: {
'@libName': path.resolve(__dirname, 'src/lib')
}
}
}
Сборка деклараций #
На текущий момент наш production-бандл — это js-файл, который ничего не знает о типах. Чтобы это исправить давайте включим в состав нашего репозитория директорию с типами. В tsconfig.json необходимо указать директорию, в которую TypeScript будет собирать типы.
{
"compilerOptions": {
"declarationDir": "lib"
}
}
Теперь декларации будут генерироваться в каталог lib относительно корня проекта.
И тут, как это обычно бывает, в нашей бочке меда появляется ложечка дегтя. Мы использовали alias’ы и поэтому в декларации попадут alias’ы, про которые проект, в котором будет использоваться библиотека, ничего не знает. А значит и типизация работать не будет.
Чтобы это исправить воспользуемся пакетом ttypescript и плагином для нее typescript-transform-path.
Теперь нам осталось только прописать скрипт для генерации деклараций:
{
"scripts": {
"declarations": "ttsc --emitDeclarationOnly"
}
}
Теперь перед выпуском новой версии библиотеки вам необходимо запускать команду:
npm run declarations
Внесем последние штрихи в наш package.json
.
Заполните еще два ключа:
- main — главный файл библиотеки (пропишите путь к вашему production-бандлу)
- types — главный файл деклараций
Версионирование #
Для выпуска новой версии воспользуйтесь командой npm version
. Результатом выполнения этой команды станет commit, помеченный тегом с новой версией библиотеки.
Распространение #
Напомню, что нам необходимо установить библиотеку как npm-зависимость из приватного git-репозитория (желательно через ssh). Для этого, находясь в проекте, куда требуется установить вашу новоиспеченную библиотеку, просто выполните команду:
npm install git+ssh://git@github.com:/[#semver:^x.x.x]
Разумеется, вместо github.com вы можете прописать путь к вашему удаленному репозиторию. Я умышленно не хочу заморачиваться с поднятием приватного npm-сервера. Мне кажется, что установка прямо из репозитория — наиболее простой и удобный способ.
Это мой первый опыт создания библиотеки на TypeScript. Вполне возможно, в будущем я внесу коррективы в этот workflow. Например, я хочу собирать декларации в один бандл. Как только я разберусь с этим — напишу отдельную заметку.