Monorepo

Документация

Про то, какие плюшки дает монорепозиторий писать не буду, об этом уже написано вот тут или тут и еще в куче мест.

В этой заметке я опишу, как создать монорепозиторий, используя npm workspaces (доступны с npm v7), предже всего это делается для того, чтобы избежать лишних vendor lock'ов.

То есть идея заключается в том, что есть npm и он занимается менеджментом зависимостей, а также линкует мои пакеты между собой, что позволит мне удобно переиспользовать код.

Здесь не будет затронута тема автоматического выпуска версий пакетов, деплоя и наверное еще много чего, опишу то, что лично я буду использовать, по ходу буду добавлять заметки, в том числе и для себя самого.

Начало

Создаем директорию для нашей мнонорепы, переходим в нее cd /myrepo и создаем корневой package.json.

npm init -y

Немного подредактируем package.json: Делаем его приватным, и указываем, где будут располагаться наши workspaces (далее я буду называть их пакетами) - я указал директорию apps для приложений и packages для пакетов/библиотек, которые могут поставляться как обычные npm пакеты:

"name": "@myrepo/root",
"private": true,
"workspaces": ["apps/*", "packages/**"],

Пример из жизни (про packages/**) : на работе есть проект, в котором один из пакетов - библиотека React компонентов (своего рода пакет с пакетами), которая собирается как в один пакет со всеми компонентами, так и в несколько пакетов (кнопка отдельно, инпут отдельно и так далее), все это поставляется как отдельные npm пакеты и у каждого компонента могут быть свои зависимости.

Поэтому, если я захочу написать в рамках монорепы свою библиотеку компонентов - то мой package.json уже готов к этому 😁

Добавляем packages

В моем случае ситуация следующая:

  • одно реакт приложение на Next.js
  • один сервис на NodeJS
  • prisma ORM, схему и клиент для которой будут использовать два других пакета.
  • Один npm пакет/библиотека (пока приватный)

Процесс переноса существующих приложений не должен занять много времени, в моем случае мне потребовалось выполнить следующие шаги:

  • Скопировать весь код проектов кроме package-lock.json и node_modules в соотвествующие директории и packages и немного поменять их package.json:
//apps/web-app/package.json
"name": "@myrepo/web-app",
"private": true // Тут в зависимости от того, хотите ли вы публиковать пакет в npm или нет

//apps/node-app/package.json
"name": "@myrepo/node-app",
"private": true // Тут в зависимости от того, хотите ли вы публиковать пакет в npm или нет

//packages/flowchart/package.json
"name": "@myrepo/flowchart",
"private": true // Тут в зависимости от того, хотите ли вы публиковать пакет в npm или нет
  • Вынес общие dependencies и devDependencies в корневой package.json (у которого name: @myrepo/root)
  • Добавил файлик .nvmrc в корень проекта с таким содержимым (ваша версия ноды может отличаться):
16.15.0

Это нужно чтобы на моем ноуте и на сервере все работало на одной и той же версии ноды. Чтобы переключать версии ноды у вас также должен быть установлен сам nvm

Все что связано с prisma можно пропустить, если вам не надо*

  • Вынес все, что касается prisma из packages/web-app в packages/prisma (но package.json, как для отдельного пакета создавать не стал, работает нормально и без него), получилась такая структура:

    • ./packages/prisma
      • schema.prisma

    У меня уже есть заметка про: Как подключиться к MYSQL БД хостинга Beget.ru Поэтому файлик только один :)

  • В корневой package.json добавляем следующую строчку, чтобы указать, где сейчас следует искать схему призмы для базы данных:

// ./package.json
"prisma": {
	"schema": "packages/prisma/schema.prisma"
},
  • Находясь в корне проекта устанавливаем зависимости (после этого у нас появится единственная папка node_modules в корне нашей монорепы):

    • nvm use - чтобы выбрать версию ноды в .nvmrc
    • npm install - эту же команду надо запускать, каждый раз, когда добавляете очередной пакет(workspace), чтобы npm правильно слинковал пакеты
  • Так как призма генерует клиента в папку node_modules/.prisma/client, а папок node_modules внутри packages у нас больше нет, то везде, где мы ипортируем import { PrismaClient } from "@prisma/client"; - увидим ошибки (по крайней мере, если у вас typescript), поэтому теперь надо сгенерировать клиента для призмы заново:

    npx prisma generate

Дальше было немного специфичного для меня рефакторинга, который не имеет смысл описывать, в целом на этом этапе основа заложена:

  • все зависимости хранятся в одном месте - есть одна корневая директория node_modules
  • есть возможность без проблем шарить и переиспользовать код из пакетов так, как если бы это были обычные npm пакеты (например import { some } from '@myrepo/some-package')

Полезные команды для монорепы

  1. Запустить во всех воркспейсах какой-то скрипт:

npm run some-script --workspaces --if-present .

Если не указать --if-present, то команда будет падать с ошибкой, если не во всех воркспейсах есть скрипт. Таким образом я могу запустить сборку сразу нескольких связанных проектов в монорепозитории одной командой, просто добавить одну строчку в поле scripts корневого package.json:

"scripts": {
	"build": "npm run build --workspaces --if-present"
}

После этого достаточно набрать в терминале npm run dev:apps

  1. Установка пакетов в один из пакетов (воркспейсов)
npm install somePackage -w=@myrepo/some-workspace

Вместо заключения

К посту прикреплена ссылка на монорепозиторий, с настроенной esbuild сборкой для каждой директории внутри packages, он может послужить отправной точкой, если хотите например запилить ui кит, где каждый компонент будет собираться в отдельный пакет