Недавно я впервые прошел увлекательный путь по выделению кода из проекта на 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. Например, я хочу собирать декларации в один бандл. Как только я разберусь с этим — напишу отдельную заметку.