Монорепо: typescpript & workspaces npm. Настройка и публикация в npm

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Существуют разные способы создания монорепозитория в 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
└── packages

Я еще добавил tsconfig, но скорее всего на этом уровне в нем нет необходимости.

Теперь необходимо в каталоге packages(или той/тех, который у вас указаны в workspaces) создать каталоги - ваши пакеты в составе этого репо. У меня это app, types(тут предполагаются DTO) и helpers(еще один пакет, просто для разнообразия).

В каждом каталоге проинициализировать npm пакет, соответственно появятся package.json и добавить свой tsconfig файл.

Вообще говоря, можно использовать один tsconfig файл и положить его для всех файлов в одном месте, но я решил сделать по файлу на пакет, пусть пока конфига и одинаковая.

В итоге у меня получилась вот такая структура:

├── package.json
├── tsconfig.json
└── packages
    ├── app
    │   ├── index.ts
    │   ├── tsconfig.json
    │   └── package.json
    ├── types
    │   ├── index.ts
    │   ├── tsconfig.json
    │   └── package.json
    └── helpers
        ├── index.ts
        ├── tsconfig.json
        └── 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.

Спасибо за внимание, надеюсь, кому-то эта информация окажется полезной!

Источник: https://habr.com/ru/post/540114/


Интересные статьи

Интересные статьи

Эта статья будет если не последней в нашем импровизированном цикле, то во всяком случае у меня не скоро накопится материал на следующую. Речь пойдёт сначала об IP-сети на...
В этой инструкции показывается как настроить пользователей только для чтения в PostgreSQL для Redash. Читать дальше →
Итак, в прошлом посте я рассказал вам что что такое Armory Engine. Сейчас расскажу вам как установить движок и сделать свой первый тестовый уровень (в следующем уроке). Перед тем как на...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
Часть первая. Вводная Часть вторая. Настройка правил Firewall и NAT Часть третья. Настройка DHCP NSX Edge поддерживает статическую и динамическую (ospf, bgp) маршрутизацию. Первоначальн...