Существуют разные способы создания монорепозитория в node.js, есть разные библиотеки для этих целей: yarn workspaces, lerna и так далее. Но сегодня я хочу коротко рассказать о монорепозитории на typescript, используя только npm.
Монорепозиторий в данном случае - это единый репозиторий, содержащий в себе несколько различных пакетов, каждый из которых может подключаться отдельно и тянуть только свои зависимости.
Если не хочется читать процесс, в конце есть ссылка на созданный мной простейший монорепозиторий на typescript, можно посмотреть на примере.
Предыстория.
У нас появилась идея сделать общие DTO для фронта и бэка. На бэке 2 языка - JavaScript/TypeScript + Java. Плюс хотим и пробуем автогенерить http клиентов, но пока не очень надо.
В итоге у нас есть openApi yaml файлики с описанием DTO и интерфейсов для клиентской библиотеки, по ним автогенерирую интерфейсы и типы typescript и после они компилятся в js + .d.ts. Также есть написанная мной реализация для отправки в очередь Rabbit.
Подробнее про автогенерацию рассказывать в рамках данной статьи не буду, но если кому будет интересно - могу написать короткую статью по этой теме.
Сами DTO - повторюсь - просто автогенерируемые интерфейсы, они не тянут никаких зависимостей(ну разве что typescript, но он и так во всех приложениях-потребителях уже есть), а вот Rabbit клиент - уже тянет. И если на бэке лишний вес - особо не проблема, то наш фронт тоже хочет использовать DTO. И там лишний вес - плохо(спасибо, кэп)). И в Рэббит ему тоже отправлять ничего не надо.
Так родилась идея разделить на пакеты. Но разделять на репозитории нам не хотелось. Пусть клиент лежит вместе с DTO.
Итого, нам нужен монорепо с несколькими пакетами, причем один пакет(или несколько) тянет зависимостью другой(или несколько) внутри репозитория.
Подобное можно реализовать с помощью yarn&workspaces, но у нас инфраструктура завязана на npm, так что ничего менять не хотелось. Плюс еще предстоить публиковать в свой локальный нексус, там еще предстоит разбираться.
Итого имеем typescript-пакеты и npm. Можно еще lerna, собственно с нее я и хотел начать, но перед этим полез смотреть, а как решена проблема у других.
Первым делом полез в lodash , ведь я знаю, что там можно подключать каждую функцию отдельно. Но ответа там не нашел. На очереди babel. И там просто зайдя в репозиторий, увидел один из коммитов с выпиливанием какой-то части lerna. Пошарив по babel, я не нашел следов lerna. На этом тему с lerna решил закрыть и поисcледовать, а как можно это сделать без использования сторонних библиотек.
И тут в игру вступает workspaces. Это в моем понимании и есть различные пакеты(различные рабочие пространства) внутри одного репо.
Задача сводится к 1.реализовать монорепу, 2.опубликовать так, чтобы каждый пакет внутри был доступен как отдельный пакет со своими зависимостямями.
Сразу оговорюсь, проект еще допиливается, например в части правильной структуризации зависимостей, peerDependencies, все такое, но уже представляет собой законченную единицу примера.
1.Реализация монорепозитория
Итак, ранее workspaces не было в npm, но с версии 7 эта возможность появилась, поэтому первым делом нужно проверить версию и если ниже 7, то поставить 7:
npm install -g npm@7
Или поставить nodejs 15.
Прежде, чем рассказывать далее, хочу заметить, что в качестве основы мной была использована статья https://habr.com/ru/post/448766/
В статье есть некоторые подробности, например про @ перед именем пакетов.
А мой репо получился путем форка репо (там javascript) автора статьи @PavelSmolinи превращением его в typescript либу, а так же непосредственно публикацией в npm.
Пользуясь случаем, хочу выразить @PavelSmolinсвою благодарность.
Продолжим.
Инициализируем npm пакет.
npm init
В сгенерированном package.json нужно прописать имя пакета, для примера это будет workspaces-example;
“name”: “workspaces-example”
И прописать свойство workspaces, где указать директорию, в которой будут лежать наши пакеты, обычно это директория packages:
“workspaces”:[
“./packages/*”
]
Можно указать несколько папок(например в babel их несколько) просто перечислением в массиве через запятую.
Библиотека, будучи пакетом, требует указания в package.json точки входа в пакет в свойстве main, точки входа в файлы/файл типизации(для typescript библиотеки), это свойство types.
А так же файлы и каталоги, которые должны попасть в либу при публикации в npm, для этого есть свойство files.
Точку входа в данном корневом package.json я не указываю, т.к. корень у меня не самостоятельный пакет(хотя я и опубликовал его).
Аналогично и с файлами декларации типов(у нас же ts библиотека)
files тоже пустой - файлов и каталогов нет у корня нет.
Корневой пакет - особо и не пакет. По крайней мере в описанном примере. Его можно сделать пакетом, тогда надо заполнить эти три поля: files, types, main.
Итого корневой каталог на данной стадии имеет вот такую структур
+-- package.json
L-- packages
Я еще добавил tsconfig, но скорее всего на этом уровне в нем нет необходимости.
Теперь необходимо в каталоге packages(или той/тех, который у вас указаны в workspaces) создать каталоги - ваши пакеты в составе этого репо. У меня это app, types(тут предполагаются DTO) и helpers(еще один пакет, просто для разнообразия).
В каждом каталоге проинициализировать npm пакет, соответственно появятся package.json и добавить свой tsconfig файл.
Вообще говоря, можно использовать один tsconfig файл и положить его для всех файлов в одном месте, но я решил сделать по файлу на пакет, пусть пока конфига и одинаковая.
В итоге у меня получилась вот такая структура:
+-- package.json
+-- tsconfig.json
L-- packages
+-- app
¦ +-- index.ts
¦ +-- tsconfig.json
¦ L-- package.json
+-- types
¦ +-- index.ts
¦ +-- tsconfig.json
¦ L-- package.json
L-- helpers
+-- index.ts
+-- tsconfig.json
L-- package.json
В каждом пакете мне необходимо компилировать typescript код в javascript + файлы типизации .d.ts.
Делаю это стандартно
tsc
Для этого нужно или поставить зависимостью typescript или установить его глобально.
Код генерируется в директорию dist каждого пакета:
packages/app/dist
packages/types/dist
packages/types/dist
Имя директории, куда генерировать указывается в tsconfig.json
“compilerOptions”: {
“outDir”: “dist”
}
Чтобы генерировались файлы декларации типов в соответствующий tsconfig.json надо указать
“compilerOptions”: {
“declaration”: true
}
В моем случае точкой входа в каждый пакет является файл index.ts(на схеме выше видно), поэтому я заполняю каждый package.json соответствующими значениями полей types, files и main:
“types”: “dist/index.d.ts”
“main”: “dist/index.js”
“files”: [
“dist”
]
Обратите внимание, в main расширение .js, это уже javascript.
Дальше интереснее.
Чтобы правильно линковать пакеты внутри репо в каждом пакете внутри каталога packages в его paсkage.json я указываю в имени пакета отсылку к имени корня:
“name”: “@workspaces-example/<имя пакета>
Например для пакета app, это поле будет
“name”: “@workspaces-example/app
Аналогично у types и helpers(для моего примера)
Так же добавляю информацию о репозитории пакета в раздел “repository” соответствующего файлика package.json. Обратите внимание на "directory". Здесь лежит путь к пакету, подробнее тут.
Для app это выглядит вот так:
"repository": {
"type": "git",
"url": "https://github.com/<ваш id странички на гите>/workspaces-example.git",
"directory": "packages/app"
}
Здесь стоит обратить внимание: чтобы опубликовать пакет, он не должен быть приватным, у меня это решено вот так в package.json соответствующего пакета:
"publishConfig": {
"access": "public"
}
И последний шаг по настройке каждого пакета - это добавление зависимостей.
У меня helpers не имеет внутренних зависимостей, types тоже, а вот в app используются типы из @workspaces-example/types и что-то из @workspaces-example/helpers:
"dependencies": {
"@workspaces-example/types": "<версия>",
"@workspaces-example/helpers": "<версия>"
}
На данном этапе файл packages/app/package.json выглядит следующим образом
Если вы нигде не ошиблись, то теперь в корне проекта выполняем
npm i
И все зависимости пакетов линкуются(напомню, пока сторонних зависимостей, включая typescript в проекте нет).
Теперь внутри app можно подключать внутренние пакеты, например вот так:
import {typeA, typeB, interfaceA} from '@workspaces-example/types'
Естественно в @workspaces-example/types должны быть описаны эти типы и интерфейс и собраны в types/dist
В принципе, на этом настройка работы нескольких пакетов в одном репозитории закончена.
2.Публикация в npm
Для публикации пакета в npm необходимо зарегистрироваться в npm.
Далее необходимо опубликовать каждый пакет в составе репо.
Для этого надо выполнить
npm publish
в директории каждого пакета в составе репо. Но пока не спешите этого делать, сейчас ничего(кроме корня) не опубликуется.
Для публикации подобного монорепо с несколькими пакетами придется в своем профиле на npm создать организацию.
Создаем организацию workspaces-example, при создании выбираем бесплатный вариант.
Переходим в каждый проект и выполняем
npm publish
Не забываем перед каждой новой публикацией поднимать версию публикуемого пакета.
Теперь каждый пакет можно установить в любое свое приложение из npm, путем выполнения стандартной команды, например
npm i @workspaces-example/types
Далее, как любит говорить один известный автор на youtube: "В принципе на этом все.")
Немного про пакет-пример.
Хочу отметить, что в моем тестовом пакете пока неразбериха с зависимостями, дублируется tsconfig.
Также я не храню в гите директорию dist(добавлена в .gitignore), а генерирую ее при установке пакета зависимостью с помощью npm хука prepape в секции scripts соответствующего пакета.
Выглядит это так
"scripts": {
"build": "tsc",
"prepare": "npm run build"
}
В дальнейшем будем с коллегами прикручивать наш локальный нексус, привет Миша!
Пример созданного и опубликованного пакета monorepo-typescript
Ссылка на гит: https://github.com/euhoo/monorepo-typescript
Я буду рад в комментариях почитать полезную информацию или исправление неточностей, а также если поделятся какими-то альтернативными способами публикации подобных пакетов, кроме как создание организации в npm.
Спасибо за внимание, надеюсь, кому-то эта информация окажется полезной!