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

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